From 4675aff76828b0747d1ac900d65d4a92a457ebf5 Mon Sep 17 00:00:00 2001 From: Jonas Bernoulli Date: Tue, 5 Dec 2023 19:59:34 +0100 Subject: [PATCH] Update to Transient v0.5.2 --- doc/misc/transient.texi | 1046 ++++++++++--------------- lisp/transient.el | 1620 ++++++++++++++++++++++----------------- 2 files changed, 1331 insertions(+), 1335 deletions(-) diff --git a/doc/misc/transient.texi b/doc/misc/transient.texi index e06f7759d1b..b6c426d7f21 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.4.3 +@subtitle for version 0.5.2 @author Jonas Bernoulli @page @vskip 0pt plus 1filll @@ -44,37 +44,16 @@ General Public License for more details. @node Top @top Transient User and Developer Manual -Taking inspiration from prefix keys and prefix arguments, Transient -implements a similar abstraction involving a prefix command, infix -arguments and suffix commands. We could call this abstraction a -``transient command'', but because it always involves at least two -commands (a prefix and a suffix) we prefer to call it just a -``transient''. - -When the user calls a transient prefix command, a transient -(temporary) keymap is activated, which binds the transient's infix -and suffix commands, and functions that control the transient state -are added to @code{pre-command-hook} and @code{post-command-hook}. The available -suffix and infix commands and their state are shown in a popup buffer -until the transient is exited by invoking a suffix command. - -Calling an infix command causes its value to be changed, possibly by -reading a new value in the minibuffer. +Transient is the library used to implement the keyboard-driven ``menus'' +in Magit. It is distributed as a separate package, so that it can be +used to implement similar menus in other packages. -Calling a suffix command usually causes the transient to be exited -but suffix commands can also be configured to not exit the transient. - -@quotation -The second part of this manual, which describes how to modify existing -transients and create new transients from scratch, can be hard to -digest if you are just getting started. A useful resource to get over -that hurdle is Psionic K's interactive tutorial, available at -@uref{https://github.com/positron-solutions/transient-showcase}. - -@end quotation +This manual can be bit hard to digest when getting started. A useful +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.4.3. +This manual is for Transient version 0.5.2. @insertcopying @end ifnottex @@ -85,7 +64,6 @@ This manual is for Transient version 0.4.3. * Modifying Existing Transients:: * Defining New Commands:: * Classes and Methods:: -* Related Abstractions and Packages:: * FAQ:: * Keystroke Index:: * Command and Function Index:: @@ -110,6 +88,7 @@ Usage Defining New Commands +* Technical Introduction:: * Defining Transients:: * Binding Suffix and Infix Commands:: * Defining Suffix and Infix Commands:: @@ -139,169 +118,91 @@ Suffix Methods * Suffix Format Methods:: -Related Abstractions and Packages - -* Comparison With Prefix Keys and Prefix Arguments:: -* Comparison With Other Packages:: - @end detailmenu @end menu @node Introduction @chapter Introduction -Taking inspiration from prefix keys and prefix arguments, Transient -implements a similar abstraction involving a prefix command, infix -arguments and suffix commands. We could call this abstraction a -``transient command'', but because it always involves at least two -commands (a prefix and a suffix) we prefer to call it just a -``transient''. - -@cindex transient prefix command -@quotation -Transient keymaps are a feature provided by Emacs. Transients as -implemented by this package involve the use of transient keymaps. - -Emacs provides a feature that it calls @dfn{prefix commands}. When we -talk about ``prefix commands'' in this manual, then we mean our own kind -of ``prefix commands'', unless specified otherwise. To avoid ambiguity -we sometimes use the terms @dfn{transient prefix command} for our kind and -``regular prefix command'' for Emacs' kind. - -@end quotation - -When the user calls a transient prefix command, a transient -(temporary) keymap is activated, which binds the transient's infix and -suffix commands, and functions that control the transient state are -added to @code{pre-command-hook} and @code{post-command-hook}. The available suffix -and infix commands and their state are shown in a popup buffer until -the transient state is exited by invoking a suffix command. - -Calling an infix command causes its value to be changed. How that is -done depends on the type of the infix command. The simplest case is -an infix command that represents a command-line argument that does not -take a value. Invoking such an infix command causes the switch to be -toggled on or off. More complex infix commands may read a value from -the user, using the minibuffer. - -Calling a suffix command usually causes the transient to be exited; -the transient keymaps and hook functions are removed, the popup buffer -no longer shows information about the (no longer bound) suffix -commands, the values of some public global variables are set, while -some internal global variables are unset, and finally the command is -actually called. Suffix commands can also be configured to not exit -the transient. - -A suffix command can, but does not have to, use the infix arguments in -much the same way any command can choose to use or ignore the prefix -arguments. For a suffix command that was invoked from a transient, the -variable @code{transient-current-suffixes} and the function @code{transient-args} -serve about the same purpose as the variables @code{prefix-arg} and -@code{current-prefix-arg} do for any command that was called after the prefix -arguments have been set using a command such as @code{universal-argument}. +Transient is the library used to implement the keyboard-driven @dfn{menus} +in Magit. It is distributed as a separate package, so that it can be +used to implement similar menus in other packages. -The information shown in the popup buffer while a transient is active -looks a bit like this: - -@example -,----------------------------------------- -|Arguments -| -f Force (--force) -| -a Annotate (--annotate) -| -|Create -| t tag -| r release -`----------------------------------------- -@end example +This manual can be bit hard to digest when getting started. A useful +resource to get over that hurdle is Psionic K's interactive tutorial, +available at @uref{https://github.com/positron-solutions/transient-showcase}. -@quotation -This is a simplified version of @code{magit-tag}. Info manuals do not -support images or colored text, so the above ``screenshot'' lacks some -information; in practice you would be able to tell whether the -arguments @code{--force} and @code{--annotate} are enabled or not based on their -color. +@anchor{Some things that Transient can do} +@heading Some things that Transient can do -@end quotation - -@cindex command dispatchers -Transient can be used to implement simple ``command dispatchers''. The -main benefit then is that the user can see all the available commands -in a popup buffer. That is useful by itself because it frees the user -from having to remember all the keys that are valid after a certain -prefix key or command. Magit's @code{magit-dispatch} (on @kbd{C-x M-g}) command is -an example of using Transient to merely implement a command -dispatcher. - -In addition to that, Transient also allows users to interactively pass -arguments to commands. These arguments can be much more complex than -what is reasonable when using prefix arguments. There is a limit to -how many aspects of a command can be controlled using prefix -arguments. Furthermore, what a certain prefix argument means for -different commands can be completely different, and users have to read -documentation to learn and then commit to memory what a certain prefix -argument means to a certain command. - -Transient suffix commands, on the other hand, can accept dozens of -different arguments without the user having to remember anything. -When using Transient, one can call a command with arguments that are -just as complex as when calling the same function non-interactively -from Lisp. - -Invoking a transient suffix command with arguments is similar to -invoking a command in a shell with command-line completion and history -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. See @xref{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 (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}. +@itemize +@item +Display current state of arguments +@item +Display and manage lifecycle of modal bindings +@item +Contextual user interface +@item +Flow control for wizard-like composition of interactive forms +@item +History & persistence +@item +Rendering arguments for controlling CLI programs +@end itemize -Transient ships with support for a few different types of specialized -infix commands. A command that sets a command line option, for example, -has different needs than a command that merely toggles a boolean flag. -Additionally, Transient provides abstractions for defining new types, -which the author of Transient did not anticipate (or didn't get around -to implementing yet). - -Note that suffix commands also support regular prefix arguments. A -suffix command may even be called with both infix and prefix arguments -at the same time. If you invoke a command as a suffix of a transient -prefix command, but also want to pass prefix arguments to it, then -first invoke the prefix command, and only after doing that invoke the -prefix arguments, before finally invoking the suffix command. If you -instead began by providing the prefix arguments, then those would -apply to the prefix command, not the suffix command. Likewise, if you -want to change infix arguments before invoking a suffix command with -prefix arguments, then change the infix arguments before invoking the -prefix arguments. In other words, regular prefix arguments always -apply to the next command, and since transient prefix, infix and -suffix commands are just regular commands, the same applies to them. -(Regular prefix keys behave differently because they are not commands -at all, instead they are just incomplete key sequences, and those -cannot be interrupted with prefix commands.) +@anchor{Complexity in CLI programs} +@heading Complexity in CLI programs + +Complexity tends to grow with time. How do you manage the complexity +of commands? Consider the humble shell command @samp{ls}. It now has over +@emph{fifty} command line options. Some of these are boolean flags (@samp{ls -l}). +Some take arguments (@samp{ls --sort=s}). Some have no effect unless paired +with other flags (@samp{ls -lh}). Some are mutually exclusive. Some shell +commands even have so many options that they introduce @emph{subcommands} +(@samp{git branch}, @samp{git commit}), each with their own rich set of options +(@samp{git branch -f}). + +@anchor{Using Transient for composing interactive commands} +@heading Using Transient for composing interactive commands + +What about Emacs commands used interactively? How do these handle +options? One solution is to make many versions of the same command, +so you don't need to! Consider: @samp{delete-other-windows} vs. +@samp{delete-other-windows-vertically} (among many similar examples). + +Some Emacs commands will simply prompt you for the next "argument" +(@samp{M-x switch-to-buffer}). Another common solution is to use prefix +arguments which usually start with @samp{C-u}. Sometimes these are sensibly +numerical in nature (@samp{C-u 4 M-x forward-paragraph} to move forward 4 +paragraphs). But sometimes they function instead as boolean +"switches" (@samp{C-u C-SPACE} to jump to the last mark instead of just +setting it, @samp{C-u C-u C-SPACE} to unconditionally set the mark). Since +there aren't many standards for the use of prefix options, you have to +read the command's documentation to find out what the possibilities +are. + +But when an Emacs command grows to have a truly large set of options +and arguments, with dependencies between them, lots of option values, +etc., these simple approaches just don't scale. Transient is designed +to solve this issue. Think of it as the humble prefix argument @samp{C-u}, +@emph{raised to the power of 10}. Like @samp{C-u}, it is key driven. Like the +shell, it supports boolean "flag" options, options that take +arguments, and even "sub-commands", with their own options. But +instead of searching through a man page or command documentation, +well-designed transients @emph{guide} their users to the relevant set of +options (and even their possible values!) directly, taking into +account any important pre-existing Emacs settings. And while for +shell commands like @samp{ls}, there is only one way to "execute" (hit +@samp{Return}!), transients can "execute" using multiple different keys tied +to one of many self-documenting @emph{actions} (imagine having 5 different +colored return keys on your keyboard!). Transients make navigating +and setting large, complex groups of command options and arguments +easy. Fun even. Once you've tried it, it's hard to go back to the +@samp{C-u what can I do here again?} way. @node Usage @chapter Usage -@menu -* Invoking Transients:: -* Aborting and Resuming Transients:: -* Common Suffix Commands:: -* Saving Values:: -* Using History:: -* Getting Help for Suffix Commands:: -* Enabling and Disabling Suffixes:: -* Other Commands:: -* Configuration:: -@end menu - @node Invoking Transients @section Invoking Transients @@ -366,7 +267,7 @@ it returns to the previous transient, if any. Transient's predecessor bound @kbd{q} instead of @kbd{C-g} to the quit command. To learn how to get that binding back see @code{transient-bind-q-to-quit}'s -doc string. +documentation string. @table @asis @item @kbd{C-q} (@code{transient-quit-all}) @@ -458,7 +359,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}). @table @asis @item @kbd{C-x s} (@code{transient-set}) @@ -519,8 +420,8 @@ to cycle through 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. @@ -562,8 +463,8 @@ What sort of documentation is shown depends on how the transient was defined. For infix commands that represent command-line arguments this ideally shows the appropriate manpage. @code{transient-help} then tries to jump to the correct location within that. Info manuals are also -supported. The fallback is to show the command's doc string, for -non-infix suffixes this is usually appropriate. +supported. The fallback is to show the command's documentation +string, for non-infix suffixes this is usually appropriate. @node Enabling and Disabling Suffixes @section Enabling and Disabling Suffixes @@ -637,6 +538,13 @@ not. The predicates also apply in edit mode. Therefore, to control which suffixes are available given a certain state, you have to make sure that that state is currently active. + +@item @kbd{C-x a} (@code{transient-toggle-level-limit}) +@kindex C-x a +@findex transient-toggle-level-limit +This command toggle whether suffixes that are on levels lower than +the level specified by @code{transient-default-level} are temporarily +available anyway. @end table @node Other Commands @@ -727,7 +635,7 @@ buffer. The transient popup 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: @@ -742,8 +650,7 @@ The default is: This displays the window at the bottom of the selected frame. Another useful @var{FUNCTION} is @code{display-buffer-below-selected}, which is what @code{magit-popup} used by default. For more alternatives see -@ref{Buffer Display Action Functions,,,elisp,}, and see @ref{Buffer Display -Action Alists,,,elisp,}. +@ref{Buffer Display Action Functions,,,elisp,}, and @ref{Buffer Display Action Alists,,,elisp,}. Note that the buffer that was current before the transient buffer is shown should remain the current buffer. Many suffix commands @@ -782,27 +689,30 @@ If @code{nil}, then the buffer has no mode-line. If the buffer is not displayed right above the echo area, then this probably is not a good value. -If @code{line} (the default), then the buffer also has no mode-line, but a -thin line is drawn instead, using the background color of the face -@code{transient-separator}. Text-mode frames cannot display thin lines, -and therefore fall back to treating @code{line} like @code{nil}. +If @code{line} (the default) or a natural number, then the buffer +has no mode-line, but a line is drawn is drawn in its place. +If a number is used, that specifies the thickness of the line. +On termcap frames we cannot draw lines, so there @code{line} and +numbers are synonyms for @code{nil}. -Otherwise this can be any mode-line format. @xref{Mode Line -Format,,,elisp,}, for details. +The color of the line is used to indicate if non-suffixes are +allowed and whether they exit the transient. The foreground +color of @code{transient-key-noop} (if non-suffix are disallowed), +@code{transient-key-stay} (if allowed and transient stays active), or +@code{transient-key-exit} (if allowed and they exit the transient) is +used to draw the line. + +Otherwise this can be any mode-line format. See @ref{Mode Line Format,,,elisp,}, for details. @end defopt @defopt transient-semantic-coloring -This option controls whether prefixes and suffixes are colored in -a Hydra-like fashion. +This option controls whether colors are used to indicate the +transient behavior of commands. If non-@code{nil}, then the key binding of each suffix is colorized to indicate whether it exits the transient state or not. The color of the prefix is indicated using the line that is drawn when the value of @code{transient-mode-line-format} is @code{line}. - -For more information about how Hydra uses colors see -@uref{https://github.com/abo-abo/hydra#color} and -@uref{https://oremacs.com/2015/02/19/hydra-colors-reloaded}. @end defopt @defopt transient-highlight-mismatched-keys @@ -941,10 +851,10 @@ The following functions share a few arguments: 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 command, a key vector, a key description (a string as @@ -1014,13 +924,105 @@ signal an error. @node Defining New Commands @chapter Defining New Commands -@menu -* Defining Transients:: -* Binding Suffix and Infix Commands:: -* Defining Suffix and Infix Commands:: -* Using Infix Arguments:: -* Transient State:: -@end menu +@node Technical Introduction +@section Technical Introduction + +Taking inspiration from prefix keys and prefix arguments, Transient +implements a similar abstraction involving a prefix command, infix +arguments and suffix commands. + +When the user calls a transient prefix command, a transient +(temporary) keymap is activated, which binds the transient's infix and +suffix commands, and functions that control the transient state are +added to @code{pre-command-hook} and @code{post-command-hook}. The available suffix +and infix commands and their state are shown in a popup buffer until +the transient state is exited by invoking a suffix command. + +Calling an infix command causes its value to be changed. How that is +done depends on the type of the infix command. The simplest case is +an infix command that represents a command-line argument that does not +take a value. Invoking such an infix command causes the switch to be +toggled on or off. More complex infix commands may read a value from +the user, using the minibuffer. + +Calling a suffix command usually causes the transient to be exited; +the transient keymaps and hook functions are removed, the popup buffer +no longer shows information about the (no longer bound) suffix +commands, the values of some public global variables are set, while +some internal global variables are unset, and finally the command is +actually called. Suffix commands can also be configured to not exit +the transient. + +A suffix command can, but does not have to, use the infix arguments in +much the same way any command can choose to use or ignore the prefix +arguments. For a suffix command that was invoked from a transient, the +variable @code{transient-current-suffixes} and the function @code{transient-args} +serve about the same purpose as the variables @code{prefix-arg} and +@code{current-prefix-arg} do for any command that was called after the prefix +arguments have been set using a command such as @code{universal-argument}. + +@cindex command dispatchers +Transient can be used to implement simple ``command dispatchers''. The +main benefit then is that the user can see all the available commands +in a popup buffer, which can be thought of as a ``menus''. That is +useful by itself because it frees the user from having to remember all +the keys that are valid after a certain prefix key or command. +Magit's @code{magit-dispatch} (on @kbd{C-x M-g}) command is an example of using +Transient to merely implement a command dispatcher. + +In addition to that, Transient also allows users to interactively pass +arguments to commands. These arguments can be much more complex than +what is reasonable when using prefix arguments. There is a limit to +how many aspects of a command can be controlled using prefix +arguments. Furthermore, what a certain prefix argument means for +different commands can be completely different, and users have to read +documentation to learn and then commit to memory what a certain prefix +argument means to a certain command. + +Transient suffix commands, on the other hand, can accept dozens of +different arguments without the user having to remember anything. +When using Transient, one can call a command with arguments that are +just as complex as when calling the same function non-interactively +from Lisp. + +Invoking a transient suffix command with arguments is similar to +invoking a command in a shell with command-line completion and history +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. 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 (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. +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, +has different needs than a command that merely toggles a boolean flag. +Additionally, Transient provides abstractions for defining new types, +which the author of Transient did not anticipate (or didn't get around +to implementing yet). + +Note that suffix commands also support regular prefix arguments. A +suffix command may even be called with both infix and prefix arguments +at the same time. If you invoke a command as a suffix of a transient +prefix command, but also want to pass prefix arguments to it, then +first invoke the prefix command, and only after doing that invoke the +prefix arguments, before finally invoking the suffix command. If you +instead began by providing the prefix arguments, then those would +apply to the prefix command, not the suffix command. Likewise, if you +want to change infix arguments before invoking a suffix command with +prefix arguments, then change the infix arguments before invoking the +prefix arguments. In other words, regular prefix arguments always +apply to the next command, and since transient prefix, infix and +suffix commands are just regular commands, the same applies to them. +(Regular prefix keys behave differently because they are not commands +at all, instead they are just incomplete key sequences, and those +cannot be interrupted with prefix commands.) @node Defining Transients @section Defining Transients @@ -1032,7 +1034,7 @@ binds the transient's infix and suffix commands. In other words, it defines the complete transient, not just the transient prefix command that is used to invoke that transient. -@defmac transient-define-prefix name arglist [docstring] [keyword value]@dots{} group@dots{} [body@dots{}] +@defmac transient-define-prefix name arglist [docstring] [keyword value]... group... [body...] This macro defines @var{NAME} as a transient prefix command and binds the transient's infix and suffix commands. @@ -1047,7 +1049,7 @@ explicitly. @var{GROUP}s add key bindings for infix and suffix commands and specify how these bindings are presented in the popup 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: @@ -1082,15 +1084,13 @@ the branch whose variables are being configured. @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 their arguments, which has the same form as -the specifications used in @code{transient-define-prefix}. +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}. @menu * Group Specifications:: @@ -1119,13 +1119,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. @@ -1230,9 +1227,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 @@ -1243,18 +1238,18 @@ 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 @var{KEY} is the key binding, either a vector or key description string. @item @var{DESCRIPTION} is the description, either a string or a function that -returns a string. The function should be a lambda expression to -avoid ambiguity. In some cases a symbol that is bound as a function -would also work but to be safe you should use @code{:description} in that -case. +takes zero or one arguments (the suffix object) and returns a string. +The function should be a lambda expression to avoid ambiguity. In +some cases a symbol that is bound as a function would also work but +to be safe you should use @code{:description} in that case. @end itemize The next element is either a command or an argument. This is the only @@ -1322,7 +1317,7 @@ Note that an infix is a special kind of suffix. Depending on context ``suffixes'' means ``suffixes (including infixes)'' or ``non-infix suffixes''. -@defmac transient-define-suffix name arglist [docstring] [keyword value]@dots{} body@dots{} +@defmac transient-define-suffix name arglist [docstring] [keyword value]... body... This macro defines @var{NAME} as a transient suffix command. @var{ARGLIST} are the arguments that the command takes. @@ -1339,7 +1334,7 @@ The infix arguments are usually accessed by using @code{transient-args} inside @code{interactive}. @end defmac -@defmac transient-define-infix name arglist [docstring] [keyword value]@dots{} +@defmac transient-define-infix name arglist [docstring] [keyword value]... This macro defines @var{NAME} as a transient infix command. @var{ARGLIST} is always ignored (but mandatory never-the-less) and @@ -1376,7 +1371,7 @@ define your own infix command class. In that case you have to use value of the @code{:transient} keyword. @end defmac -@defmac transient-define-argument name arglist [docstring] [keyword value]@dots{} +@defmac transient-define-argument name arglist [docstring] [keyword value]... This macro defines @var{NAME} as a transient infix command. This is an alias for @code{transient-define-infix}. Only use this alias @@ -1475,63 +1470,55 @@ transient keymap is disallowed and trying to do so results in a warning. This does not ``deactivate'' the transient. @end itemize -But these are just the defaults. Whether a certain command -deactivates or ``exits'' the transient is configurable. There is more -than one way in which a command can be ``transient'' or ``non-transient''; -the exact behavior is implemented by calling a so-called ``pre-command'' -function. Whether non-suffix commands are allowed to be called is -configurable per transient. +The behavior can be changed for all suffixes of a particular prefix +and/or for individual suffixes. The values should nearly always be +booleans, but certain functions, called ``pre-commands'', can also be +used. These functions are named @code{transient--do-VERB}, and the symbol +@code{VERB} can be used as a shorthand. -@itemize -@item -The transient-ness of suffix commands (including infix commands) is -controlled by the value of their @code{transient} slot, which can be set -either when defining the command or when adding a binding to a -transient while defining the respective transient prefix command. +A boolean is interpreted as answering the question "does the +transient stay active, when this command is invoked?" @code{t} means that +the transient stays active, while @code{nil} means that invoking the command +exits the transient. -Valid values are booleans and the pre-commands described below. +Note that when the suffix is a ``sub-prefix'', invoking that command +always activates that sub-prefix, causing the outer prefix to no +longer be active and displayed. Here @code{t} means that when you exit the +inner prefix, then the outer prefix becomes active again, while @code{nil} +means that all outer prefixes are exited at once. @itemize @item -@code{t} is equivalent to @code{transient--do-stay}. -@item -@code{nil} is equivalent to @code{transient--do-exit}. -@item -If @code{transient} is unbound (and that is actually the default for -non-infix suffixes) then the value of the prefix's -@code{transient-suffix} slot is used instead. The default value of that -slot is @code{nil}, so the suffix's @code{transient} slot being unbound is -essentially equivalent to it being @code{nil}. -@end itemize +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. See +@ref{Pre-commands for Non-Suffixes}. @item -A suffix command can be a prefix command itself, i.e., a -``sub-prefix''. While a sub-prefix is active we nearly always want -@kbd{C-g} to take the user back to the ``super-prefix''. However in rare -cases this may not be desirable, and that makes the following -complication necessary: +The common behavior for the suffixes of a particular prefix can be +set using the prefix's @code{transient-suffixes} slot. -For @code{transient-suffix} objects the @code{transient} slot is unbound. We can -ignore that for the most part because, as stated above, @code{nil} and the -slot being unbound are equivalent, and mean ``do exit''. That isn't -actually true for suffixes that are sub-prefixes though. For such -suffixes unbound means ``do exit but allow going back'', which is the -default, while @code{nil} means ``do exit permanently'', which requires that -slot to be explicitly set to that value. +The value specified in this slot does @strong{not} affect infixes. Because +it affects both regular suffixes as well as sub-prefixes, which +have different needs, it is best to avoid explicitly specifying a +function. @item -The transient-ness of certain built-in suffix commands is specified -using @code{transient-predicate-map}. This is a special keymap, which -binds commands to pre-commands (as opposed to keys to commands) and -takes precedence over the @code{transient} slot. +The behavior of an individual suffix can be changed using its +@code{transient} slot. While it is usually best to use a boolean, for this +slot it can occasionally make sense to specify a function explicitly. + +Note that this slot can be set when defining a suffix command using +@code{transient-define-suffix} and/or in the definition of the prefix. If +set in both places, then the latter takes precedence, as usual. @end itemize -The available pre-command functions are documented below. They are -called by @code{transient--pre-command}, a function on @code{pre-command-hook} and -the value that they return determines whether the transient is exited. -To do so the value of one of the constants @code{transient--exit} or -@code{transient--stay} is used (that way we don't have to remember if @code{t} means -``exit'' or ``stay''). +The available pre-command functions are documented in the following +sub-sections. They are called by @code{transient--pre-command}, a function +on @code{pre-command-hook}, and the value that they return determines whether +the transient is exited. To do so the value of one of the constants +@code{transient--exit} or @code{transient--stay} is used (that way we don't have to +remember if @code{t} means ``exit'' or ``stay''). Additionally, these functions may change the value of @code{this-command} (which explains why they have to be called using @code{pre-command-hook}), @@ -1539,11 +1526,39 @@ call @code{transient-export}, @code{transient--stack-zap} or @code{transient--st and set the values of @code{transient--exitp}, @code{transient--helpp} or @code{transient--editp}. +For completeness sake, some notes about complications: + +@itemize +@item +The transient-ness of certain built-in suffix commands is specified +using @code{transient-predicate-map}. This is a special keymap, which +binds commands to pre-commands (as opposed to keys to commands) and +takes precedence over the prefix's @code{transient-suffix} slot, but not +the suffix's @code{transient} slot. + +@item +While a sub-prefix is active we nearly always want @kbd{C-g} to take the +user back to the ``super-prefix'', even when the other suffixes don't +do that. However, in rare cases this may not be desirable, and that +makes the following complication necessary: + +For @code{transient-suffix} objects the @code{transient} slot is unbound. We can +ignore that for the most part because @code{nil} and the slot being unbound +are treated as equivalent, and mean ``do exit''. That isn't actually +true for suffixes that are sub-prefixes though. For such suffixes +unbound means ``do exit but allow going back'', which is the default, +while @code{nil} means ``do exit permanently'', which requires that slot to +be explicitly set to that value. +@end itemize + @anchor{Pre-commands for Infixes} @subheading Pre-commands for Infixes The default for infixes is @code{transient--do-stay}. This is also the only -function that makes sense for infixes. +function that makes sense for infixes, which is why this predicate is +used even if the value of the prefix's @code{transient-suffix} slot is @code{t}. In +extremely rare cases, one might want to use something else, which can +be done by setting the infix's @code{transient} slot directly. @defun transient--do-stay Call the command without exporting variables and stay transient. @@ -1554,23 +1569,16 @@ Call the command without exporting variables and stay transient. By default, invoking a suffix causes the transient to be exited. -If you want a different default behavior for a certain transient -prefix command, then set its @code{:transient-suffix} slot. The value can be -a boolean, answering the question "does the transient stay active, -when a suffix command is invoked?" @code{t} means that the transient stays -active, while @code{nil} means that invoking a suffix exits the transient. -In either case, the exact behavior depends on whether the suffix is -itself a prefix (i.e., a sub-prefix), an infix or a regular suffix. - The behavior for an individual suffix command can be changed by -setting its @code{transient} slot to one of the following pre-commands. +setting its @code{transient} slot to a boolean (which is highly recommended), +or to one of the following pre-commands. @defun transient--do-exit Call the command after exporting variables and exit the transient. @end defun @defun transient--do-return -Call the command after exporting variables and return to parent +Call the command after exporting variables and return to the parent prefix. If there is no parent prefix, then call @code{transient--do-exit}. @end defun @@ -1578,9 +1586,10 @@ prefix. If there is no parent prefix, then call @code{transient--do-exit}. Call the command after exporting variables and stay transient. @end defun -The following pre-commands are suitable for sub-prefixes. Only the -first should ever explicitly be set as the value of the @code{transient} -slot. +The following pre-commands are only suitable for sub-prefixes. It is +not necessary to explicitly use these predicates because the correct +predicate is automatically picked based on the value of the @code{transient} +slot for the sub-prefix itself. @defun transient--do-recurse Call the transient prefix command, preparing for return to active @@ -1588,15 +1597,25 @@ transient. Whether we actually return to the parent transient is ultimately under the control of each invoked suffix. The difference between -this pre-command and @code{transient--do-replace} is that it changes the -value of the @code{transient-suffix} slot to @code{transient--do-return}. +this pre-command and @code{transient--do-stack} is that it changes the +value of the @code{transient-suffix} slot to @code{t}. If there is no parent transient, then only call this command and skip the second step. @end defun +@defun transient--do-stack +Call the transient prefix command, stacking the active transient. +Push the active transient to the transient stack. + +Unless @code{transient--do-recurse} is explicitly used, this pre-command +is automatically used for suffixes that are prefixes themselves, +i.e., for sub-prefixes. +@end defun + @defun transient--do-replace Call the transient prefix command, replacing the active transient. +Do not push the active transient to the transient stack. Unless @code{transient--do-recurse} is explicitly used, this pre-command is automatically used for suffixes that are prefixes themselves, @@ -1618,17 +1637,17 @@ By default, non-suffixes (commands that are bound in other keymaps beside the transient keymap) cannot be invoked. Trying to invoke such a command results in a warning and the transient stays active. -If you want a different behavior, then set the @code{:transient-non-suffix} -slot of the transient prefix command. The value can be a boolean, -answering the question, "is it allowed to invoke non-suffix commands?" +If you want a different behavior, then set the @code{transient-non-suffix} +slot of the transient prefix command. The value should be a boolean, +answering the question, "is it allowed to invoke non-suffix commands?, +a pre-command function, or a shorthand for such a function. -If the value is @code{t} or @code{transient--do-stay}, then non-suffixes can be -invoked, when it is @code{nil} or @code{transient--do-warn} (the default) then they -cannot be invoked. +If the value is @code{t}, then non-suffixes can be invoked, when it is @code{nil} +(the default) then they cannot be invoked. -The only other recommended value is @code{transient--do-leave}. If that is -used, then non-suffixes can be invoked, but if one is invoked, then -that exits the transient. +The only other recommended value is @code{leave}. If that is used, then +non-suffixes can be invoked, but if one is invoked, then that exits +the transient. @defun transient--do-warn Call @code{transient-undefined} and stay transient. @@ -1829,7 +1848,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, @@ -1843,7 +1862,7 @@ 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 @@ -1870,6 +1889,24 @@ indicates that all remaining arguments are files. @item Classes used for infix commands that represent variables should derived from the abstract @code{transient-variable} class. + +@item +The @code{transient-information} class is special in that suffixes that use +this class are not associated with a command and thus also not with +any key binding. Such suffixes are only used to display arbitrary +information, and that anywhere a suffix can appear. Display-only +suffix specifications take this form: + +@lisp +([LEVEL] :info DESCRIPTION [KEYWORD VALUE]...) +@end lisp + +The @code{:info} keyword argument replaces the @code{:description} keyword used for +other suffix classes. Other keyword arguments that you might want to +set, include @code{:face}, predicate keywords (such as @code{:if}), and @code{:format}. +By default the value of @code{:format} includes @code{%k}, which for this class is +replaced with the empty string or spaces, if keys are being padded in +the containing group. @end itemize Magit defines additional classes, which can serve as examples for the @@ -1990,12 +2027,13 @@ Show help for the prefix, infix or suffix command represented by For prefixes, show the info manual, if that is specified using the @code{info-manual} slot. Otherwise, show the manpage if that is specified -using the @code{man-page} slot. Otherwise, show the command's doc string. +using the @code{man-page} slot. Otherwise, show the command's +documentation string. -For suffixes, show the command's doc string. +For suffixes, show the command's documentation string. For infixes, show the manpage if that is specified. Otherwise show -the command's doc string. +the command's documentation string. @end defun @node Prefix Slots @@ -2017,13 +2055,26 @@ probably don't want that. @code{transient-suffix} and @code{transient-non-suffix} 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}. +command is invoked. See @ref{Transient State}. + +@item +@code{refresh-suffixes} Normally suffix objects and keymaps are only setup +once, when the prefix is invoked. Setting this to @code{t}, causes them to +be recreated after every command. This is useful when using @code{:if...} +predicates, and those need to be rerun for some reason. Doing this +is somewhat costly, and there is a risk of losing state, so this is +disabled by default and still considered experimental. @item @code{incompatible} A list of lists. Each sub-list specifies a set of mutually exclusive arguments. Enabling one of these arguments causes the others to be disabled. An argument may appear in -multiple sub-lists. +multiple sub-lists. Arguments must me given in the same form as +used in the @code{argument} or @code{argument-format} slot of the respective +suffix objects, usually something like @code{--switch} or @code{--option=%s}. For +options and @code{transient-switches} suffixes it is also possible to match +against a specific value, as returned by @code{transient-infix-value}, +for example, @code{--option=one}. @item @code{scope} For some transients it might be necessary to have a sort of @@ -2050,7 +2101,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 @@ -2083,7 +2134,7 @@ Also see @ref{Suffix Classes}. @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 popup buffer. @@ -2099,8 +2150,14 @@ It must contain the following %-placeholders: @end itemize @item -@code{description} The description, either a string or a function that is -called with no argument and returns a string. +@code{description} The description, either a string or a function, which is +called with zero or one argument (the suffix object), and returns a +string. + +@item +@code{face} Face used for the description. In simple cases it is easier +to use this instead of using a function as @code{description} and adding +the styling there. @code{face} is appended using @code{add-face-text-property}. @item @code{show-help} A function used to display help for the suffix. If @@ -2189,8 +2246,10 @@ function that takes the object as the only argument and which returns a prompt string. @item -@code{choices} A list of valid values. How exactly that is used depends on -the class of the object. +@code{choices} A list of valid values, or a function that returns such a +list. The latter is not implemented for @code{transient-switches}, because +I couldn't think of a use-case. How exactly the choices are used +varies depending on the class of the suffix. @end itemize @anchor{Slots of @code{transient-variable}} @@ -2241,337 +2300,16 @@ what happens if you use more than one. @code{if-not-derived} Enable if major-mode does not derive from value. @end itemize +By default these predicates run when the prefix command is invoked, +but this can be changes, using the @code{refresh-suffixes} prefix slot. +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}. - -@node Related Abstractions and Packages -@chapter Related Abstractions and Packages - -@menu -* Comparison With Prefix Keys and Prefix Arguments:: -* Comparison With Other Packages:: -@end menu - -@node Comparison With Prefix Keys and Prefix Arguments -@section Comparison With Prefix Keys and Prefix Arguments - -While transient commands were inspired by regular prefix keys and -prefix arguments, they are also quite different and much more complex. - -The following diagrams illustrate some of the differences. - -@itemize -@item -@samp{(c)} represents a return to the command loop. -@item -@samp{(+)} represents the user's choice to press one key or another. -@item -@samp{@{WORD@}} are possible behaviors. -@item -@samp{@{NUMBER@}} is a footnote. -@end itemize - -@anchor{Regular Prefix Commands} -@subheading Regular Prefix Commands - -@xref{Prefix Keys,,,elisp,}. - -@example - ,--> command1 --> (c) - | -(c)-(+)-> prefix command or key --+--> command2 --> (c) - | - `--> command3 --> (c) -@end example - -@anchor{Regular Prefix Arguments} -@subheading Regular Prefix Arguments - -@xref{Prefix Command Arguments,,,elisp,}. - -@example - ,----------------------------------, - | | - v | -(c)-(+)---> prefix argument command --(c)-(+)-> any command --> (c) - | ^ | - | | | - `-- sets or changes --, ,-- maybe used --' | - | | | - v | | - prefix argument state | - ^ | - | | - `-------- discards --------' -@end example - -@anchor{Transients} -@subheading Transients - -(∩`-´)⊃━☆゚.*・。゚ - -This diagram ignores the infix value and external state: - -@example -(c) - | ,- @{stay@} ------<-,-<------------<-,-<---, -(+) | | | | - | | | | | - | | ,--> infix1 --| | | - | | | | | | - | | |--> infix2 --| | | - v v | | | | - prefix -(c)-(+)-> infix3 --' ^ | - | | | - |---------------> suffix1 -->--| | - | | | - |---------------> suffix2 ----@{1@}------> @{exit@} --> (c) - | | - |---------------> suffix3 -------------> @{exit@} --> (c) - | | - `--> any command --@{2@}-> @{warn@} -->--| - | | - |--> @{noop@} -->--| - | | - |--> @{call@} -->--' - | - `------------------> @{exit@} --> (c) -@end example - -This diagram takes the infix value into account to an extend, while -still ignoring external state: - -@example -(c) - | ,- @{stay@} ------<-,-<------------<-,-<---, -(+) | | | | - | | | | | - | | ,--> infix1 --| | | - | | | | | | | - | | ,--> infix2 --| | | - v v | | | | | - prefix -(c)-(+)-> infix3 --' | | - | | ^ | - | | | | - |---------------> suffix1 -->--| | - | | ^ | | - | | | | | - |---------------> suffix2 ----@{1@}------> @{exit@} --> (c) - | | ^ | | - | | | | v - | | | | | - |---------------> suffix3 -------------> @{exit@} --> (c) - | | ^ | | - | sets | | v - | | maybe | | - | | used | | - | | | | | - | | infix --' | | - | `---> value | | - | ^ | | - | | | | - | hides | | - | | | | - | `--------------------------<---| - | | | - `--> any command --@{2@}-> @{warn@} -->--| | - | | | - |--> @{noop@} -->--| | - | | | - |--> @{call@} -->--' ^ - | | - `------------------> @{exit@} --> (c) -@end example - -This diagram provides more information about the infix value -and also takes external state into account. - -@example - ,----sets--- "anything" - | - v - ,---------> external - | state - | | | - | initialized | ☉‿⚆ - sets from | - | | maybe - | ,----------' used - | | | -(c) | | v - | ,- @{stay@} --|---<-,-<------|-----<-,-<---, -(+) | | | | | | | - | | | v | | | | - | | ,--> infix1 --| | | | - | | | | | | | | | - | | | | v | | | | - | | ,--> infix2 --| | | | - | | | | ^ | | | | - v v | | | | | | | - prefix -(c)-(+)-> infix3 --' | | | - | | ^ | ^ | - | | | v | | - |---------------> suffix1 -->--| | - | | | ^ | | | - | | | | v | | - |---------------> suffix2 ----@{1@}------> @{exit@} --> (c) - | | | ^ | | | - | | | | | | v - | | | | v | | - |---------------> suffix3 -------------> @{exit@} --> (c) - | | | ^ | | - | sets | | | v - | | initialized maybe | | - | | from used | | - | | | | | | - | | `-- infix ---' | | - | `---> value -----------------------------> persistent - | ^ ^ | | across - | | | | | invocations -, - | hides | | | | - | | `----------------------------------------------' - | | | | - | `--------------------------<---| - | | | - `--> any command --@{2@}-> @{warn@} -->--| | - | | | - |--> @{noop@} -->--| | - | | | - |--> @{call@} -->--' ^ - | | - `------------------> @{exit@} --> (c) -@end example - -@itemize -@item -@samp{@{1@}} Transients can be configured to be exited when a suffix command -is invoked. The default is to do so for all suffixes except for -those that are common to all transients and which are used to -perform tasks such as providing help and saving the value of the -infix arguments for future invocations. The behavior can also be -specified for individual suffix commands and may even depend on -state. - -@item -@samp{@{2@}} Transients can be configured to allow the user to invoke -non-suffix commands. The default is to not allow that and instead -warn the user. -@end itemize - -Despite already being rather complex, even the last diagram leaves out -many details. Most importantly it implies that the decision whether -to remain transient is made later than it actually is made (for the -most part a function on @code{pre-command-hook} is responsible). But such -implementation details are of little relevance to users and are -covered elsewhere. - -@node Comparison With Other Packages -@section Comparison With Other Packages - -@anchor{Magit-Popup} -@subheading Magit-Popup - -Transient is the successor to Magit-Popup (@pxref{Top,,,magit-popup,}). - -One major difference between these two implementations of the same -ideas is that while Transient uses transient keymaps and embraces the -command-loop, Magit-Popup implemented an inferior mechanism that does -not use transient keymaps and that instead of using the command-loop -implements a naive alternative based on @code{read-char}. - -Magit-Popup does not use classes and generic functions and defining a -new command type is near impossible as it involves adding hard-coded -special-cases to many functions. Because of that only a single new -type was added, which was not already part of Magit-Popup's initial -release. - -A lot of things are hard-coded in Magit-Popup. One random example is -that the key bindings for switches must begin with @code{-} and those for -options must begin with @code{=}. - -@anchor{Hydra} -@subheading Hydra - -Hydra (see @uref{https://github.com/abo-abo/hydra}) is another package that -provides features similar to those of Transient. - -Both packages use transient keymaps to make a set of commands -temporarily available and show the available commands in a popup -buffer. - -A Hydra ``body'' is equivalent to a Transient ``prefix'' and a Hydra -``head'' is equivalent to a Transient ``suffix''. Hydra has no equivalent -of a Transient ``infix''. - -Both hydras and transients can be used as simple command dispatchers. -Used like this they are similar to regular prefix commands and prefix -keys, except that the available commands are shown in the popup buffer. - -(Another package that does this is @code{which-key}. It does so automatically -for any incomplete key sequence. The advantage of that approach is -that no additional work is necessary; the disadvantage is that the -available commands are not organized semantically.) - -Both Hydra and Transient provide features that go beyond simple -command dispatchers: - -@itemize -@item -Invoking a command from a hydra does not necessarily exit the hydra. -That makes it possible to invoke the same command again, but using a -shorter key sequence (i.e., the key that was used to enter the hydra -does not have to be pressed again). - -Transient supports that too, but for now this feature is not a focus -and the interface is a bit more complicated. A very basic example -using the current interface: - -@lisp -(transient-define-prefix outline-navigate () - :transient-suffix 'transient--do-stay - :transient-non-suffix 'transient--do-warn - [("p" "previous visible heading" outline-previous-visible-heading) - ("n" "next visible heading" outline-next-visible-heading)]) -@end lisp - -@item -Transient supports infix arguments; values that are set by infix -commands and then consumed by the invoked suffix command(s). - -To my knowledge, Hydra does not support that. -@end itemize - -Both packages make it possible to specify how exactly the available -commands are outlined: - -@itemize -@item -With Hydra this is often done using an explicit format string, which -gives authors a lot of flexibility and makes it possible to do fancy -things. - -The downside of this is that it becomes harder for a user to add -additional commands to an existing hydra and to change key bindings. - -@item -Transient allows the author of a transient to organize the commands -into groups and the use of generic functions allows authors of -transients to control exactly how a certain command type is -displayed. - -However while Transient supports giving sections a heading it does -not currently support giving the displayed information more -structure by, for example, using box-drawing characters. - -That could be implemented by defining a new group class, which lets -the author specify a format string. It should be possible to -implement that without modifying any existing code, but it does not -currently exist. -@end itemize +See @ref{Enabling and Disabling Suffixes}. @node FAQ @appendix FAQ @@ -2584,10 +2322,10 @@ Yes, see @code{transient-display-buffer-action} in @ref{Configuration}. @anchor{How can I copy text from the popup buffer?} @appendixsec How can I copy text from the popup buffer? -To be able to mark text in any transient popup buffer using the mouse, -you have to add the following binding. Note that the region won't be -visualized, while doing so. After you have quit the transient popup, -you will be able to yank it another buffer. +To be able to mark text in Transient's popup 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 popup, you will be able to yank it in another buffer. @lisp (keymap-set transient-predicate-map @@ -2595,6 +2333,16 @@ you will be able to yank it another buffer. #'transient--do-stay) @end lisp +@anchor{How does Transient compare to prefix keys and universal arguments?} +@appendixsec How does Transient compare to prefix keys and universal arguments? + +See @uref{https://github.com/magit/transient/wiki/Comparison-with-prefix-keys-and-universal-arguments}. + +@anchor{How does Transient compare to Magit-Popup and Hydra?} +@appendixsec How does Transient compare to Magit-Popup and Hydra? + +See @uref{https://github.com/magit/transient/wiki/Comparison-with-other-packages}. + @anchor{Why did some of the key bindings change?} @appendixsec Why did some of the key bindings change? @@ -2657,7 +2405,7 @@ for @kbd{q}. If you want to get @kbd{q}'s old binding back then you can do so. Doing that is a bit more complicated than changing a single key binding, so I have implemented a function, @code{transient-bind-q-to-quit} that makes the -necessary changes. See its doc string for more information. +necessary changes. See its documentation string for more information. @node Keystroke Index @appendix Keystroke Index diff --git a/lisp/transient.el b/lisp/transient.el index dd2b4e0db0b..f17323bec4f 100644 --- a/lisp/transient.el +++ b/lisp/transient.el @@ -3,11 +3,11 @@ ;; Copyright (C) 2018-2023 Free Software Foundation, Inc. ;; Author: Jonas Bernoulli -;; URL: https://github.com/magit/transient +;; Homepage: https://github.com/magit/transient ;; Keywords: extensions -;; Package-Version: 0.4.3 -;; Package-Requires: ((emacs "26.1")) +;; Package-Version: 0.5.2 +;; Package-Requires: ((emacs "26.1") (compat "29.1.4.4") (seq "2.24")) ;; SPDX-License-Identifier: GPL-3.0-or-later @@ -28,35 +28,52 @@ ;;; Commentary: -;; Taking inspiration from prefix keys and prefix arguments, Transient -;; implements a similar abstraction involving a prefix command, infix -;; arguments and suffix commands. We could call this abstraction a -;; "transient command", but because it always involves at least two -;; commands (a prefix and a suffix) we prefer to call it just a -;; "transient". - -;; When the user calls a transient prefix command, then a transient -;; (temporary) keymap is activated, which binds the transient's infix -;; and suffix commands, and functions that control the transient state -;; are added to `pre-command-hook' and `post-command-hook'. The -;; available suffix and infix commands and their state are shown in -;; the echo area until the transient is exited by invoking a suffix -;; command. - -;; Calling an infix command causes its value to be changed, possibly -;; by reading a new value in the minibuffer. - -;; Calling a suffix command usually causes the transient to be exited -;; but suffix commands can also be configured to not exit the -;; transient state. +;; Transient is the library used to implement the keyboard-driven menus +;; in Magit. It is distributed as a separate package, so that it can be +;; used to implement similar menus in other packages. ;;; Code: (require 'cl-lib) +(require 'compat) (require 'eieio) (require 'edmacro) (require 'format-spec) + +(eval-and-compile + (when (and (featurep' seq) + (not (fboundp 'seq-keep))) + (unload-feature 'seq 'force))) (require 'seq) +(unless (fboundp 'seq-keep) + (display-warning 'transient (substitute-command-keys "\ +Transient requires `seq' >= 2.24, +but due to bad defaults, Emacs' package manager, refuses to +upgrade this and other built-in packages to higher releases +from GNU Elpa, when a package specifies that this is needed. + +To fix this, you have to add this to your init file: + + (setq package-install-upgrade-built-in t) + +Then evaluate that expression by placing the cursor after it +and typing \\[eval-last-sexp]. + +Once you have done that, you have to explicitly upgrade `seq': + + \\[package-upgrade] seq \\`RET' + +Then you also must make sure the updated version is loaded, +by evaluating this form: + + (progn (unload-feature 'seq t) (require 'seq)) + +Until you do this, you will get random errors about `seq-keep' +being undefined while using Transient. + +If you don't use the `package' package manager but still get +this warning, then your chosen package manager likely has a +similar defect.") :emergency)) (eval-when-compile (require 'subr-x)) @@ -65,10 +82,20 @@ (declare-function Man-next-section "man" (n)) (declare-function Man-getpage-in-background "man" (topic)) -(defvar display-line-numbers) ; since Emacs 26.1 (defvar Man-notify-method) (defvar pp-default-function) ; since Emacs 29.1 +(defmacro static-if (condition then-form &rest else-forms) + "A conditional compilation macro. +Evaluate CONDITION at macro-expansion time. If it is non-nil, +expand the macro to THEN-FORM. Otherwise expand it to ELSE-FORMS +enclosed in a `progn' form. ELSE-FORMS may be empty." + (declare (indent 2) + (debug (sexp sexp &rest sexp))) + (if (eval condition lexical-binding) + then-form + (cons 'progn else-forms))) + (defmacro transient--with-emergency-exit (&rest body) (declare (indent defun)) `(condition-case err @@ -198,21 +225,30 @@ If nil, then the buffer has no mode-line. If the buffer is not displayed right above the echo area, then this probably is not a good value. -If `line' (the default), then the buffer also has no mode-line, -but a thin line is drawn instead, using the background color of -the face `transient-separator'. Termcap frames cannot display -thin lines and therefore fallback to treating `line' like nil. +If `line' (the default) or a natural number, then the buffer +has no mode-line, but a line is drawn is drawn in its place. +If a number is used, that specifies the thickness of the line. +On termcap frames we cannot draw lines, so there `line' and +numbers are synonyms for nil. + +The color of the line is used to indicate if non-suffixes are +allowed and whether they exit the transient. The foreground +color of `transient-key-noop' (if non-suffix are disallowed), +`transient-key-stay' (if allowed and transient stays active), or +`transient-key-exit' (if allowed and they exit the transient) is +used to draw the line. Otherwise this can be any mode-line format. See `mode-line-format' for details." :package-version '(transient . "0.2.0") :group 'transient - :type '(choice (const :tag "hide mode-line" nil) - (const :tag "substitute thin line" line) - (const :tag "name of prefix command" - ("%e" mode-line-front-space - mode-line-buffer-identification)) - (sexp :tag "custom mode-line format"))) + :type '(choice (const :tag "hide mode-line" nil) + (const :tag "substitute thin line" line) + (number :tag "substitute line with thickness") + (const :tag "name of prefix command" + ("%e" mode-line-front-space + mode-line-buffer-identification)) + (sexp :tag "custom mode-line format"))) (defcustom transient-show-common-commands nil "Whether to show common transient suffixes in the popup buffer. @@ -236,7 +272,7 @@ of this variable use \"C-x t\" when a transient is active." This only affects infix arguments that represent command-line arguments. When this option is non-nil, then the key binding for infix argument are highlighted when only a long argument -\(e.g. \"--verbose\") is specified but no shor-thand (e.g \"-v\"). +\(e.g., \"--verbose\") is specified but no shorthand (e.g., \"-v\"). In the rare case that a short-hand is specified but does not match the key binding, then it is highlighted differently. @@ -285,19 +321,14 @@ using a layout optimized for Lisp. :group 'transient :type '(choice (const :tag "Transform no keys (nil)" nil) function)) -(defcustom transient-semantic-coloring nil - "Whether to color prefixes and suffixes in Hydra-like fashion. -This feature is experimental. +(defcustom transient-semantic-coloring t + "Whether to use colors to indicate transient behavior. If non-nil, then the key binding of each suffix is colorized to -indicate whether it exits the transient state or not. The color -of the prefix is indicated using the line that is drawn when the -value of `transient-mode-line-format' is `line'. - -For more information about how Hydra uses colors see -https://github.com/abo-abo/hydra#color and -https://oremacs.com/2015/02/19/hydra-colors-reloaded." - :package-version '(transient . "0.3.0") +indicate whether it exits the transient state or not, and the +line that is drawn below the transient popup buffer is used to +indicate the behavior of non-suffix commands." + :package-version '(transient . "0.5.0") :group 'transient :type 'boolean) @@ -356,8 +387,8 @@ text and might otherwise have to scroll in two dimensions." :group 'transient :type 'boolean) +(defconst transient--max-level 7) (defconst transient--default-child-level 1) - (defconst transient--default-prefix-level 4) (defcustom transient-default-level transient--default-prefix-level @@ -436,22 +467,18 @@ give you as many additional suffixes as you hoped.)" "Face used for headings." :group 'transient-faces) -(defface transient-key '((t :inherit font-lock-builtin-face)) - "Face used for keys." - :group 'transient-faces) - -(defface transient-argument '((t :inherit font-lock-warning-face)) +(defface transient-argument '((t :inherit font-lock-string-face :weight bold)) "Face used for enabled arguments." :group 'transient-faces) -(defface transient-value '((t :inherit font-lock-string-face)) - "Face used for values." - :group 'transient-faces) - (defface transient-inactive-argument '((t :inherit shadow)) "Face used for inactive arguments." :group 'transient-faces) +(defface transient-value '((t :inherit font-lock-string-face :weight bold)) + "Face used for values." + :group 'transient-faces) + (defface transient-inactive-value '((t :inherit shadow)) "Face used for inactive values." :group 'transient-faces) @@ -460,28 +487,14 @@ give you as many additional suffixes as you hoped.)" "Face used for suffixes unreachable from the current prefix sequence." :group 'transient-faces) -(defface transient-active-infix '((t :inherit secondary-selection)) - "Face used for the infix for which the value is being read." - :group 'transient-faces) - -(defface transient-unreachable-key '((t :inherit (transient-key shadow))) - "Face used for keys unreachable from the current prefix sequence." - :group 'transient-faces) - -(defface transient-nonstandard-key '((t :underline t)) - "Face optionally used to highlight keys conflicting with short-argument. -Also see option `transient-highlight-mismatched-keys'." - :group 'transient-faces) - -(defface transient-mismatched-key '((t :underline t)) - "Face optionally used to highlight keys without a short-argument. -Also see option `transient-highlight-mismatched-keys'." - :group 'transient-faces) - (defface transient-inapt-suffix '((t :inherit shadow :italic t)) "Face used for suffixes that are inapt at this time." :group 'transient-faces) +(defface transient-active-infix '((t :inherit highlight)) + "Face used for the infix for which the value is being read." + :group 'transient-faces) + (defface transient-enabled-suffix '((t :background "green" :foreground "black" :weight bold)) "Face used for enabled levels while editing suffix levels. @@ -494,63 +507,83 @@ See info node `(transient)Enabling and Disabling Suffixes'." See info node `(transient)Enabling and Disabling Suffixes'." :group 'transient-faces) -(defface transient-higher-level '((t :underline t)) +(defface transient-higher-level + `((t :box ( :line-width ,(if (>= emacs-major-version 28) (cons -1 -1) -1) + :color ,(let ((color (face-attribute 'shadow :foreground nil t))) + (or (and (not (eq color 'unspecified)) color) + "grey60"))))) "Face optionally used to highlight suffixes on higher levels. Also see option `transient-highlight-higher-levels'." :group 'transient-faces) -(defface transient-separator - `((((class color) (background light)) - ,@(and (>= emacs-major-version 27) '(:extend t)) - :background "grey80") - (((class color) (background dark)) - ,@(and (>= emacs-major-version 27) '(:extend t)) - :background "grey30")) - "Face used to draw line below transient popup window. -This is only used if `transient-mode-line-format' is `line'. -Only the background color is significant." +(defface transient-delimiter '((t :inherit shadow)) + "Face used for delimiters and separators. +This includes the parentheses around values and the pipe +character used to separate possible values from each other." :group 'transient-faces) -(defgroup transient-color-faces - '((transient-semantic-coloring custom-variable)) - "Faces used by Transient for Hydra-like command coloring. -These faces are only used if `transient-semantic-coloring' -\(which see) is non-nil." +(defface transient-key '((t :inherit font-lock-builtin-face)) + "Face used for keys." :group 'transient-faces) -(defface transient-red - '((t :inherit transient-key :foreground "red")) - "Face used for red prefixes and suffixes." - :group 'transient-color-faces) +(defface transient-key-stay + `((((class color) (background light)) + :inherit transient-key + :foreground "#22aa22") + (((class color) (background dark)) + :inherit transient-key + :foreground "#ddffdd")) + "Face used for keys of suffixes that don't exit transient state." + :group 'transient-faces) -(defface transient-blue - '((t :inherit transient-key :foreground "blue")) - "Face used for blue prefixes and suffixes." - :group 'transient-color-faces) +(defface transient-key-noop + `((((class color) (background light)) + :inherit transient-key + :foreground "grey80") + (((class color) (background dark)) + :inherit transient-key + :foreground "grey30")) + "Face used for keys of suffixes that currently cannot be invoked." + :group 'transient-faces) -(defface transient-amaranth - '((t :inherit transient-key :foreground "#E52B50")) - "Face used for amaranth prefixes." - :group 'transient-color-faces) +(defface transient-key-return + `((((class color) (background light)) + :inherit transient-key + :foreground "#aaaa11") + (((class color) (background dark)) + :inherit transient-key + :foreground "#ffffcc")) + "Face used for keys of suffixes that return to the parent transient." + :group 'transient-faces) -(defface transient-pink - '((t :inherit transient-key :foreground "#FF6EB4")) - "Face used for pink prefixes." - :group 'transient-color-faces) +(defface transient-key-exit + `((((class color) (background light)) + :inherit transient-key + :foreground "#aa2222") + (((class color) (background dark)) + :inherit transient-key + :foreground "#ffdddd")) + "Face used for keys of suffixes that exit transient state." + :group 'transient-faces) -(defface transient-teal - '((t :inherit transient-key :foreground "#367588")) - "Face used for teal prefixes." - :group 'transient-color-faces) +(defface transient-unreachable-key + '((t :inherit (shadow transient-key) :weight normal)) + "Face used for keys unreachable from the current prefix sequence." + :group 'transient-faces) -(defface transient-purple - '((t :inherit transient-key :foreground "#a020f0")) - "Face used for purple prefixes. +(defface transient-nonstandard-key + `((t :box ( :line-width ,(if (>= emacs-major-version 28) (cons -1 -1) -1) + :color "cyan"))) + "Face optionally used to highlight keys conflicting with short-argument. +Also see option `transient-highlight-mismatched-keys'." + :group 'transient-faces) -This is an addition to the colors supported by Hydra. It is -used by suffixes that quit the current prefix but return to -the previous prefix." - :group 'transient-color-faces) +(defface transient-mismatched-key + `((t :box ( :line-width ,(if (>= emacs-major-version 28) (cons -1 -1) -1) + :color "magenta"))) + "Face optionally used to highlight keys without a short-argument. +Also see option `transient-highlight-mismatched-keys'." + :group 'transient-faces) ;;; Persistence @@ -633,6 +666,8 @@ If `transient-save-history' is nil, then do nothing." (man-page :initarg :man-page :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) + (refresh-suffixes :initarg :refresh-suffixes :initform nil) (incompatible :initarg :incompatible :initform nil) (suffix-description :initarg :suffix-description) (variable-pitch :initarg :variable-pitch :initform nil) @@ -698,7 +733,9 @@ slot is non-nil." (transient :initarg :transient) (format :initarg :format :initform " %k %d") (description :initarg :description :initform nil) + (face :initarg :face :initform nil) (show-help :initarg :show-help :initform nil) + (inapt-face :initarg :inapt-face :initform 'transient-inapt-suffix) (inapt :initform nil) (inapt-if :initarg :inapt-if @@ -734,6 +771,12 @@ slot is non-nil." :documentation "Inapt if major-mode does not derive from value.")) "Superclass for suffix command.") +(defclass transient-information (transient-suffix) + ((format :initform " %k %d") + (key :initform " ")) + "Display-only information. +A suffix object with no associated command.") + (defclass transient-infix (transient-suffix) ((transient :initform t) (argument :initarg :argument) @@ -788,8 +831,8 @@ They become the value of this argument.") ((suffixes :initarg :suffixes :initform nil) (hide :initarg :hide :initform nil) (description :initarg :description :initform nil) - (setup-children :initarg :setup-children) - (pad-keys :initarg :pad-keys)) + (pad-keys :initarg :pad-keys :initform nil) + (setup-children :initarg :setup-children)) "Abstract superclass of all group classes." :abstract t) @@ -815,7 +858,6 @@ elements themselves.") ;;; Define -;;;###autoload (defmacro transient-define-prefix (name arglist &rest args) "Define NAME as a transient prefix command. @@ -932,11 +974,11 @@ explicitly. The function definitions is always: - (lambda () - (interactive) - (let ((obj (transient-suffix-object))) - (transient-infix-set obj (transient-infix-read obj))) - (transient--show)) + (lambda () + (interactive) + (let ((obj (transient-suffix-object))) + (transient-infix-set obj (transient-infix-read obj))) + (transient--show)) `transient-infix-read' and `transient-infix-set' are generic functions. Different infix commands behave differently because @@ -973,7 +1015,16 @@ example, sets a variable, use `transient-define-infix' instead. \(fn NAME ARGLIST [DOCSTRING] [KEYWORD VALUE]...)") (defun transient--default-infix-command () - "Most transient infix commands are but an alias for this command." + ;; Most infix commands are but an alias for this command. + "Cannot show any documentation for this anonymous infix command. + +This infix command was defined anonymously, i.e., it was define +inside a call to `transient-define-prefix'. + +When you request help for such an infix command, then we usually +show the respective man-page and jump to the location where the +respective argument is being described. This isn't possible in +this case, because the `man-page' slot was not set in this case." (interactive) (let ((obj (transient-suffix-object))) (transient-infix-set obj (transient-infix-read obj))) @@ -981,30 +1032,31 @@ example, sets a variable, use `transient-define-infix' instead. (put 'transient--default-infix-command 'interactive-only t) (put 'transient--default-infix-command 'command-modes (list 'not-a-mode)) -(defun transient--expand-define-args (args &optional arglist) - (unless (listp arglist) - (error "Mandatory ARGLIST is missing")) - (let (class keys suffixes docstr) - (when (stringp (car args)) - (setq docstr (pop args))) - (while (keywordp (car args)) - (let ((k (pop args)) - (v (pop args))) - (if (eq k :class) - (setq class v) - (push k keys) - (push v keys)))) - (while (let ((arg (car args))) - (or (vectorp arg) - (and arg (symbolp arg)))) - (push (pop args) suffixes)) - (list (if (eq (car-safe class) 'quote) - (cadr class) - class) - (nreverse keys) - (nreverse suffixes) - docstr - args))) +(eval-and-compile + (defun transient--expand-define-args (args &optional arglist) + (unless (listp arglist) + (error "Mandatory ARGLIST is missing")) + (let (class keys suffixes docstr) + (when (stringp (car args)) + (setq docstr (pop args))) + (while (keywordp (car args)) + (let ((k (pop args)) + (v (pop args))) + (if (eq k :class) + (setq class v) + (push k keys) + (push v keys)))) + (while (let ((arg (car args))) + (or (vectorp arg) + (and arg (symbolp arg)))) + (push (pop args) suffixes)) + (list (if (eq (car-safe class) 'quote) + (cadr class) + class) + (nreverse keys) + (nreverse suffixes) + docstr + args)))) (defun transient--parse-child (prefix spec) (cl-etypecase spec @@ -1069,8 +1121,9 @@ example, sets a variable, use `transient-define-infix' instead. (commandp (cadr spec))) (setq args (plist-put args :description (macroexp-quote pop))))) (cond + ((eq car :info)) ((keywordp car) - (error "Need command, got `%s'" car)) + (error "Need command or `:info', got `%s'" car)) ((symbolp car) (setq args (plist-put args :command (macroexp-quote pop)))) ((and (commandp car) @@ -1088,7 +1141,10 @@ example, sets a variable, use `transient-define-infix' instead. `(prog1 ',sym (put ',sym 'interactive-only t) (put ',sym 'command-modes (list 'not-a-mode)) - (defalias ',sym ,(macroexp-quote cmd))))))) + (defalias ',sym + ,(if (eq (car-safe cmd) 'lambda) + cmd + (macroexp-quote cmd)))))))) ((or (stringp car) (and car (listp car))) (let ((arg pop) @@ -1123,6 +1179,9 @@ example, sets a variable, use `transient-define-infix' instead. (val pop)) (cond ((eq key :class) (setq class val)) ((eq key :level) (setq level val)) + ((eq key :info) + (setq class 'transient-information) + (setq args (plist-put args :description val))) ((eq (car-safe val) '\,) (setq args (plist-put args key (cadr val)))) ((or (symbolp val) @@ -1191,11 +1250,11 @@ Intended for use in a group's `:setup-children' function." (equal (transient--suffix-predicate suf) (transient--suffix-predicate conflict))))) (transient-remove-suffix prefix key)) - (cl-ecase action - (insert (setcdr mem (cons elt (cdr mem))) - (setcar mem suf)) - (append (setcdr mem (cons suf (cdr mem)))) - (replace (setcar mem suf))))))) + (pcase-exhaustive action + ('insert (setcdr mem (cons elt (cdr mem))) + (setcar mem suf)) + ('append (setcdr mem (cons suf (cdr mem)))) + ('replace (setcar mem suf))))))) ;;;###autoload (defun transient-insert-suffix (prefix loc suffix &optional keep-other) @@ -1306,7 +1365,7 @@ See info node `(transient)Modifying Existing Transients'." (delq (car (transient--group-member loc layout)) (aref layout 3))) nil) - (t (transient--group-member loc layout)))) + ((transient--group-member loc layout)))) (defun transient--group-member (loc group) (cl-member-if (lambda (suffix) @@ -1335,7 +1394,7 @@ See info node `(transient)Modifying Existing Transients'." (plist-get plist :command))))) (defun transient--command-key (cmd) - (and-let* ((obj (get cmd 'transient--suffix))) + (and-let* ((obj (transient--suffix-prototype cmd))) (cond ((slot-boundp obj 'key) (oref obj key)) ((slot-exists-p obj 'shortarg) @@ -1376,11 +1435,15 @@ variable instead.") (defconst transient--exit nil "Do exit the transient.") (defvar transient--exitp nil "Whether to exit the transient.") -(defvar transient--showp nil "Whether the transient is show in a popup buffer.") +(defvar transient--showp nil "Whether to show the transient popup buffer.") (defvar transient--helpp nil "Whether help-mode is active.") (defvar transient--editp nil "Whether edit-mode is active.") -(defvar transient--active-infix nil "The active infix awaiting user input.") +(defvar transient--refreshp nil + "Whether to refresh the transient completely.") + +(defvar transient--all-levels-p nil + "Whether temporary display of suffixes on all levels is active.") (defvar transient--timer nil) @@ -1392,7 +1455,7 @@ variable instead.") "Name of the transient buffer.") (defvar transient--window nil - "The window used to display the transient popup.") + "The window used to display the transient popup buffer.") (defvar transient--original-window nil "The window that was selected before the transient was invoked. @@ -1402,7 +1465,24 @@ Usually it remains selected while the transient is active.") "The buffer that was current before the transient was invoked. Usually it remains current while the transient is active.") -(defvar transient--debug nil "Whether put debug information into *Messages*.") +(defvar transient--restore-winconf nil + "Window configuration to restore after exiting help.") + +(defvar transient--shadowed-buffer nil + "The buffer that is temporarily shadowed by the transient buffer. +This is bound while the suffix predicate is being evaluated and while +drawing in the transient buffer.") + +(defvar transient--pending-suffix nil + "The suffix that is currently being processed. +This is bound while the suffix predicate is being evaluated.") + +(defvar transient--pending-group nil + "The group that is currently being processed. +This is bound while the suffixes are drawn in the transient buffer.") + +(defvar transient--debug nil + "Whether to put debug information into *Messages*.") (defvar transient--history nil) @@ -1414,6 +1494,31 @@ Usually it remains current while the transient is active.") ;;; Identities +(defun transient-prefix-object () + "Return the current prefix as an object. + +While a transient is being setup or refreshed (which involves +preparing its suffixes) the variable `transient--prefix' can be +used to access the prefix object. Thus this is what has to be +used in suffix methods such as `transient-format-description', +and in object-specific functions that are stored in suffix slots +such as `description'. + +When a suffix command is invoked (i.e., in its `interactive' form +and function body) then the variable `transient-current-prefix' +has to be used instead. + +Two distinct variables are needed, because any prefix may itself +be used as a suffix of another prefix, and such sub-prefixes have +to be able to tell themselves apart from the prefix they were +invoked from. + +Regular suffix commands, which are not prefixes, do not have to +concern themselves with this distinction, so they can use this +function instead. In the context of a plain suffix, it always +returns the value of the appropiate variable." + (or transient--prefix transient-current-prefix)) + (defun transient-suffix-object (&optional command) "Return the object associated with the current suffix command. @@ -1425,11 +1530,11 @@ This function is intended to be called by infix commands, which are usually aliases of `transient--default-infix-command', which is defined like this: - (defun transient--default-infix-command () - (interactive) - (let ((obj (transient-suffix-object))) - (transient-infix-set obj (transient-infix-read obj))) - (transient--show)) + (defun transient--default-infix-command () + (interactive) + (let ((obj (transient-suffix-object))) + (transient-infix-set obj (transient-infix-read obj))) + (transient--show)) \(User input is read outside of `interactive' to prevent the command from being added to `command-history'. See #23.) @@ -1474,12 +1579,17 @@ probably use this instead: (listify-key-sequence (this-command-keys)))) suffixes)) (car suffixes))) - (when-let* ((obj (get (or command this-command) 'transient--suffix)) - (obj (clone obj))) - ;; Cannot use and-let* because of debbugs#31840. - (transient-init-scope obj) - (transient-init-value obj) - obj))) + (and-let* ((obj (transient--suffix-prototype (or command this-command))) + (obj (clone obj))) + (progn ; work around debbugs#31840 + (transient-init-scope obj) + (transient-init-value obj) + obj)))) + +(defun transient--suffix-prototype (command) + (or (get command 'transient--suffix) + (seq-some (lambda (cmd) (get cmd 'transient--suffix)) + (function-alias-p command)))) ;;; Keymaps @@ -1570,7 +1680,8 @@ to `transient-predicate-map'. Also see `transient-base-map'." (if transient-show-common-commands "Hide common commands" "Show common permanently"))) - (list "C-x l" "Show/hide suffixes" #'transient-set-level)))))))) + (list "C-x l" "Show/hide suffixes" #'transient-set-level) + (list "C-x a" #'transient-toggle-level-limit)))))))) (defvar-keymap transient-popup-navigation-map :doc "One of the keymaps used when popup navigation is enabled. @@ -1588,6 +1699,16 @@ See `transient-enable-popup-navigation'." "" #'transient-push-button "" #'transient-push-button) +(defvar-keymap transient-resume-mode-map + :doc "Keymap for `transient-resume-mode'. + +This keymap remaps every command that would usually just quit the +documentation buffer to `transient-resume', which additionally +resumes the suspended transient." + " " #'transient-resume + " " #'transient-resume + " " #'transient-resume) + (defvar-keymap transient-predicate-map :doc "Base keymap used to map common commands to their transient behavior. @@ -1623,7 +1744,9 @@ of the corresponding object." "" #'transient--do-stay "" #'transient--do-stay "" #'transient--do-call + "" #'transient--do-exit "" #'transient--do-call + "" #'transient--do-exit "" #'transient--do-call "" #'transient--do-stay "" #'transient--do-stay @@ -1699,50 +1822,66 @@ of the corresponding object." map)) (defun transient--make-predicate-map () - (let ((map (make-sparse-keymap))) + (let* ((default (transient--resolve-pre-command + (oref transient--prefix transient-suffix))) + (return (and transient-current-prefix (eq default t))) + (map (make-sparse-keymap))) (set-keymap-parent map transient-predicate-map) - (when (memq (oref transient--prefix transient-non-suffix) - '(nil transient--do-warn transient--do-noop)) - (keymap-set map "" #'transient--do-suspend)) + (when (or (and (slot-boundp transient--prefix 'transient-switch-frame) + (transient--resolve-pre-command + (not (oref transient--prefix transient-switch-frame)))) + (memq (transient--resolve-pre-command + (oref transient--prefix transient-non-suffix)) + '(nil transient--do-warn transient--do-noop))) + (define-key map [handle-switch-frame] #'transient--do-suspend)) (dolist (obj transient--suffixes) (let* ((cmd (oref obj command)) - (sub-prefix (and (symbolp cmd) (get cmd 'transient--prefix) t))) + (kind (cond ((get cmd 'transient--prefix) 'prefix) + ((cl-typep obj 'transient-infix) 'infix) + (t 'suffix)))) (cond ((oref obj inapt) (define-key map (vector cmd) #'transient--do-warn-inapt)) ((slot-boundp obj 'transient) (define-key map (vector cmd) - (let ((do (oref obj transient))) - (pcase (list do sub-prefix) - ('(t t) #'transient--do-recurse) - ('(t nil) (if (cl-typep obj 'transient-infix) - #'transient--do-stay - #'transient--do-call)) - ('(nil t) #'transient--do-replace) - ('(nil nil) #'transient--do-exit) - (_ do))))) + (pcase (list kind + (transient--resolve-pre-command (oref obj transient)) + return) + (`(prefix t ,_) #'transient--do-recurse) + (`(prefix nil ,_) #'transient--do-stack) + (`(infix t ,_) #'transient--do-stay) + (`(suffix t ,_) #'transient--do-call) + ('(suffix nil t) #'transient--do-return) + (`(,_ nil ,_) #'transient--do-exit) + (`(,_ ,do ,_) do)))) ((not (lookup-key transient-predicate-map (vector cmd))) (define-key map (vector cmd) - (if sub-prefix - #'transient--do-replace - (or (oref transient--prefix transient-suffix) - #'transient--do-exit))))))) + (pcase (list kind default return) + (`(prefix ,(or 'transient--do-stay 'transient--do-call) ,_) + #'transient--do-recurse) + (`(prefix t ,_) #'transient--do-recurse) + (`(prefix ,_ ,_) #'transient--do-stack) + (`(infix ,_ ,_) #'transient--do-stay) + (`(suffix t ,_) #'transient--do-call) + ('(suffix nil t) #'transient--do-return) + (`(suffix nil ,_) #'transient--do-exit) + (`(suffix ,do ,_) do))))))) map)) (defun transient--make-redisplay-map () (setq transient--redisplay-key - (cl-case this-command - (transient-update + (pcase this-command + ('transient-update (setq transient--showp t) (setq unread-command-events (listify-key-sequence (this-single-command-raw-keys)))) - (transient-quit-seq + ('transient-quit-seq (setq unread-command-events (butlast (listify-key-sequence (this-single-command-raw-keys)) 2)) (butlast transient--redisplay-key)) - (t nil))) + (_ nil))) (let ((topmap (make-sparse-keymap)) (submap (make-sparse-keymap))) (when transient--redisplay-key @@ -1786,7 +1925,7 @@ EDIT may be non-nil." (setq params (list :scope (oref transient--prefix scope)))) (transient--prefix ;; Invoked as a ":transient-non-suffix 'transient--do-{stay,call}" - ;; of an outer prefix. Unlike the usual `transient--do-replace', + ;; of an outer prefix. Unlike the usual `transient--do-stack', ;; these predicates fail to clean up after the outer prefix. (transient--pop-keymap 'transient--transient-map) (transient--pop-keymap 'transient--redisplay-map)) @@ -1797,10 +1936,8 @@ EDIT may be non-nil." ;; Returning from help to edit. (setq transient--editp t))) (transient--init-objects name layout params) + (transient--init-keymaps) (transient--history-init transient--prefix) - (setq transient--predicate-map (transient--make-predicate-map)) - (setq transient--transient-map (transient--make-transient-map)) - (setq transient--redisplay-map (transient--make-redisplay-map)) (setq transient--original-window (selected-window)) (setq transient--original-buffer (current-buffer)) (setq transient--minibuffer-depth (minibuffer-depth)) @@ -1817,8 +1954,16 @@ value. Otherwise return CHILDREN as is." (funcall (oref group setup-children) children) children)) -(defun transient--init-objects (name layout params) - (setq transient--prefix (transient--init-prefix name params)) +(defun transient--init-keymaps () + (setq transient--predicate-map (transient--make-predicate-map)) + (setq transient--transient-map (transient--make-transient-map)) + (setq transient--redisplay-map (transient--make-redisplay-map))) + +(defun transient--init-objects (&optional name layout params) + (if name + (setq transient--prefix (transient--init-prefix name params)) + (setq name (oref transient--prefix command))) + (setq transient--refreshp (oref transient--prefix refresh-suffixes)) (setq transient--layout (or layout (transient--init-suffixes name))) (setq transient--suffixes (transient--flatten-suffixes transient--layout))) @@ -1845,10 +1990,11 @@ value. Otherwise return CHILDREN as is." (cl-labels ((s (def) (cond ((stringp def) nil) + ((cl-typep def 'transient-information) nil) ((listp def) (cl-mapcan #'s def)) - ((transient-group--eieio-childp def) + ((cl-typep def 'transient-group) (cl-mapcan #'s (oref def suffixes))) - ((transient-suffix--eieio-childp def) + ((cl-typep def 'transient-suffix) (list def))))) (cl-mapcan #'s layout))) @@ -1860,31 +2006,37 @@ value. Otherwise return CHILDREN as is." (defun transient--init-group (levels spec) (pcase-let ((`(,level ,class ,args ,children) (append spec nil))) - (when-let* ((- (transient--use-level-p level)) - (obj (apply class :level level args)) - (- (transient--use-suffix-p obj)) - (suffixes (cl-mapcan (lambda (c) (transient--init-child levels c)) - (transient-setup-children obj children)))) - ;; Cannot use and-let* because of debbugs#31840. - (oset obj suffixes suffixes) - (list obj)))) + (and-let* ((- (transient--use-level-p level)) + (obj (apply class :level level args)) + (- (transient--use-suffix-p obj)) + (suffixes (cl-mapcan (lambda (c) (transient--init-child levels c)) + (transient-setup-children obj children)))) + (progn ; work around debbugs#31840 + (oset obj suffixes suffixes) + (list obj))))) (defun transient--init-suffix (levels spec) (pcase-let* ((`(,level ,class ,args) spec) (cmd (plist-get args :command)) - (level (or (alist-get cmd levels) level))) + (key (transient--kbd (plist-get args :key))) + (level (or (alist-get (cons cmd key) levels nil nil #'equal) + (alist-get cmd levels) + level))) (let ((fn (and (symbolp cmd) (symbol-function cmd)))) (when (autoloadp fn) (transient--debug " autoload %s" cmd) (autoload-do-load fn))) (when (transient--use-level-p level) - (unless (and cmd (symbolp cmd)) - (error "BUG: Non-symbolic suffix command: %s" cmd)) - (let ((obj (if-let ((proto (get cmd 'transient--suffix))) - (apply #'clone proto :level level args) - (apply class :command cmd :level level args)))) - (cond ((commandp cmd)) + (let ((obj (if (child-of-class-p class 'transient-information) + (apply class :level level args) + (unless (and cmd (symbolp cmd)) + (error "BUG: Non-symbolic suffix command: %s" cmd)) + (if-let ((proto (and cmd (transient--suffix-prototype cmd)))) + (apply #'clone proto :level level args) + (apply class :command cmd :level level args))))) + (cond ((not cmd)) + ((commandp cmd)) ((or (cl-typep obj 'transient-switch) (cl-typep obj 'transient-option)) ;; As a temporary special case, if the package was compiled @@ -1893,7 +2045,8 @@ value. Otherwise return CHILDREN as is." (defalias cmd #'transient--default-infix-command)) ((transient--use-suffix-p obj) (error "Suffix command %s is not defined or autoloaded" cmd))) - (transient--init-suffix-key obj) + (unless (cl-typep obj 'transient-information) + (transient--init-suffix-key obj)) (when (transient--use-suffix-p obj) (if (transient--inapt-suffix-p obj) (oset obj inapt t) @@ -1917,33 +2070,38 @@ value. Otherwise return CHILDREN as is." (error "No key for %s" (oref obj command)))))) (defun transient--use-level-p (level &optional edit) - (or (and transient--editp (not edit)) + (or transient--all-levels-p + (and transient--editp (not edit)) (and (>= level 1) (<= level (oref transient--prefix level))))) (defun transient--use-suffix-p (obj) - (transient--do-suffix-p - (oref obj if) - (oref obj if-not) - (oref obj if-nil) - (oref obj if-non-nil) - (oref obj if-mode) - (oref obj if-not-mode) - (oref obj if-derived) - (oref obj if-not-derived) - t)) + (let ((transient--shadowed-buffer (current-buffer)) + (transient--pending-suffix obj)) + (transient--do-suffix-p + (oref obj if) + (oref obj if-not) + (oref obj if-nil) + (oref obj if-non-nil) + (oref obj if-mode) + (oref obj if-not-mode) + (oref obj if-derived) + (oref obj if-not-derived) + t))) (defun transient--inapt-suffix-p (obj) - (transient--do-suffix-p - (oref obj inapt-if) - (oref obj inapt-if-not) - (oref obj inapt-if-nil) - (oref obj inapt-if-non-nil) - (oref obj inapt-if-mode) - (oref obj inapt-if-not-mode) - (oref obj inapt-if-derived) - (oref obj inapt-if-not-derived) - nil)) + (let ((transient--shadowed-buffer (current-buffer)) + (transient--pending-suffix obj)) + (transient--do-suffix-p + (oref obj inapt-if) + (oref obj inapt-if-not) + (oref obj inapt-if-nil) + (oref obj inapt-if-non-nil) + (oref obj inapt-if-mode) + (oref obj inapt-if-not-mode) + (oref obj inapt-if-derived) + (oref obj inapt-if-not-derived) + nil))) (defun transient--do-suffix-p (if if-not if-nil if-non-nil if-mode if-not-mode if-derived if-not-derived @@ -1959,14 +2117,15 @@ value. Otherwise return CHILDREN as is." (if-not-mode (not (if (atom if-not-mode) (eq major-mode if-not-mode) (memq major-mode if-not-mode)))) - (if-derived (if (or (atom if-derived) (>= emacs-major-version 30)) + (if-derived (if (or (atom if-derived) + (>= emacs-major-version 30)) (derived-mode-p if-derived) (apply #'derived-mode-p if-derived))) (if-not-derived (not (if (or (atom if-not-derived) (>= emacs-major-version 30)) (derived-mode-p if-not-derived) (apply #'derived-mode-p if-not-derived)))) - (t default))) + (default))) (defun transient--suffix-predicate (spec) (let ((plist (nth 2 spec))) @@ -1997,6 +2156,17 @@ value. Otherwise return CHILDREN as is." ;; that we just added. (setq transient--exitp 'replace))) +(defun transient--refresh-transient () + (transient--debug 'refresh-transient) + (transient--pop-keymap 'transient--predicate-map) + (transient--pop-keymap 'transient--transient-map) + (transient--pop-keymap 'transient--redisplay-map) + (transient--init-objects) + (transient--init-keymaps) + (transient--push-keymap 'transient--transient-map) + (transient--push-keymap 'transient--redisplay-map) + (transient--redisplay)) + (defun transient--pre-command () (transient--debug 'pre-command) (transient--with-emergency-exit @@ -2005,8 +2175,8 @@ value. Otherwise return CHILDREN as is." ;; lead to a suffix being remapped to a non-suffix. We have to undo ;; the remapping in that case. However, remapping a non-suffix to ;; another should remain possible. - (when (and (transient--get-predicate-for this-original-command 'suffix) - (not (transient--get-predicate-for this-command 'suffix))) + (when (and (transient--get-pre-command this-original-command 'suffix) + (not (transient--get-pre-command this-command 'suffix))) (setq this-command this-original-command)) (cond ((memq this-command '(transient-update transient-quit-seq)) @@ -2030,34 +2200,11 @@ value. Otherwise return CHILDREN as is." (transient--wrap-command)) (t (setq transient--exitp nil) - (let ((exitp (eq (transient--do-pre-command) transient--exit))) + (let ((exitp (eq (transient--call-pre-command) transient--exit))) (transient--wrap-command) (when exitp (transient--pre-exit))))))) -(defun transient--do-pre-command () - (if-let ((fn (transient--get-predicate-for this-command))) - (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)) - -(defun transient--get-predicate-for (cmd &optional suffix-only) - (or (ignore-errors - (lookup-key transient--predicate-map (vector cmd))) - (and (not suffix-only) - (let ((pred (oref transient--prefix transient-non-suffix))) - (pcase pred - ('t #'transient--do-stay) - ('nil #'transient--do-warn) - (_ pred)))))) - (defun transient--pre-exit () (transient--debug 'pre-exit) (transient--delete-window) @@ -2086,8 +2233,9 @@ value. Otherwise return CHILDREN as is." (and (minibuffer-selected-window) (selected-window))) (buf (window-buffer transient--window))) - ;; Only delete the window if it never showed another buffer. - (unless (eq (car (window-parameter transient--window 'quit-restore)) 'other) + ;; Only delete the window if it has never shown another buffer. + (unless (eq (car (window-parameter transient--window 'quit-restore)) + 'other) (with-demoted-errors "Error while exiting transient: %S" (delete-window transient--window))) (kill-buffer buf) @@ -2163,66 +2311,65 @@ value. Otherwise return CHILDREN as is." (remove-hook 'minibuffer-exit-hook ,exit))) ,@body))) -(defun transient--wrap-command () - (if (>= emacs-major-version 30) - (transient--wrap-command-30) - (transient--wrap-command-29))) - -(defun transient--wrap-command-30 () - (letrec - ((prefix transient--prefix) - (suffix this-command) - (advice (lambda (fn &rest args) - (interactive - (lambda (spec) - (let ((abort t)) - (unwind-protect - (prog1 (advice-eval-interactive-spec spec) - (setq abort nil)) - (when abort - (when-let ((unwind (oref prefix unwind-suffix))) - (transient--debug 'unwind-interactive) - (funcall unwind suffix)) - (advice-remove suffix advice) - (oset prefix unwind-suffix nil)))))) - (unwind-protect - (apply fn args) - (when-let ((unwind (oref prefix unwind-suffix))) - (transient--debug 'unwind-command) - (funcall unwind suffix)) - (advice-remove suffix advice) - (oset prefix unwind-suffix nil))))) - (advice-add suffix :around advice '((depth . -99))))) - -(defun transient--wrap-command-29 () - (let* ((prefix transient--prefix) - (suffix this-command) - (advice nil) - (advice-interactive - (lambda (spec) - (let ((abort t)) +(static-if (>= emacs-major-version 30) + (defun transient--wrap-command () + (cl-assert + (>= emacs-major-version 30) nil + "Emacs was downgraded, making it necessary to recompile Transient") + (letrec + ((prefix transient--prefix) + (suffix this-command) + (advice (lambda (fn &rest args) + (interactive + (lambda (spec) + (let ((abort t)) + (unwind-protect + (prog1 (advice-eval-interactive-spec spec) + (setq abort nil)) + (when abort + (when-let ((unwind (oref prefix unwind-suffix))) + (transient--debug 'unwind-interactive) + (funcall unwind suffix)) + (advice-remove suffix advice) + (oset prefix unwind-suffix nil)))))) + (unwind-protect + (apply fn args) + (when-let ((unwind (oref prefix unwind-suffix))) + (transient--debug 'unwind-command) + (funcall unwind suffix)) + (advice-remove suffix advice) + (oset prefix unwind-suffix nil))))) + (advice-add suffix :around advice '((depth . -99))))) + + (defun transient--wrap-command () + (let* ((prefix transient--prefix) + (suffix this-command) + (advice nil) + (advice-interactive + (lambda (spec) + (let ((abort t)) + (unwind-protect + (prog1 (advice-eval-interactive-spec spec) + (setq abort nil)) + (when abort + (when-let ((unwind (oref prefix unwind-suffix))) + (transient--debug 'unwind-interactive) + (funcall unwind suffix)) + (advice-remove suffix advice) + (oset prefix unwind-suffix nil)))))) + (advice-body + (lambda (fn &rest args) (unwind-protect - (prog1 (advice-eval-interactive-spec spec) - (setq abort nil)) - (when abort - (when-let ((unwind (oref prefix unwind-suffix))) - (transient--debug 'unwind-interactive) - (funcall unwind suffix)) - (advice-remove suffix advice) - (oset prefix unwind-suffix nil)))))) - (advice-body - (lambda (fn &rest args) - (unwind-protect - (apply fn args) - (when-let ((unwind (oref prefix unwind-suffix))) - (transient--debug 'unwind-command) - (funcall unwind suffix)) - (advice-remove suffix advice) - (oset prefix unwind-suffix nil))))) - (setq advice `(lambda (fn &rest args) - (interactive ,advice-interactive) - (apply ',advice-body fn args))) - (advice-add suffix :around advice '((depth . -99))))) + (apply fn args) + (when-let ((unwind (oref prefix unwind-suffix))) + (transient--debug 'unwind-command) + (funcall unwind suffix)) + (advice-remove suffix advice) + (oset prefix unwind-suffix nil))))) + (setq advice `(lambda (fn &rest args) + (interactive ,advice-interactive) + (apply ',advice-body fn args))) + (advice-add suffix :around advice '((depth . -99)))))) (defun transient--premature-post-command () (and (equal (this-command-keys-vector) []) @@ -2243,7 +2390,21 @@ value. Otherwise return CHILDREN as is." (transient--debug 'post-command) (transient--with-emergency-exit (cond (transient--exitp (transient--post-exit)) - ((eq this-command (oref transient--prefix command))) + ;; If `this-command' is the current transient prefix, then we + ;; have already taken care of updating the transient buffer... + ((and (eq this-command (oref transient--prefix command)) + ;; ... but if `prefix-arg' is non-nil, then the values + ;; of `this-command' and `real-this-command' are untrue + ;; because `prefix-command-preserve-state' changes them. + ;; We cannot use `current-prefix-arg' because it is set + ;; too late (in `command-execute'), and if it were set + ;; earlier, then we likely still would not be able to + ;; rely on it and `prefix-command-preserve-state-hook' + ;; would have to be used to record that a universal + ;; argument is in effect. + (not prefix-arg))) + (transient--refreshp + (transient--refresh-transient)) ((let ((old transient--redisplay-map) (new (transient--make-redisplay-map))) (unless (equal old new) @@ -2283,6 +2444,7 @@ value. Otherwise return CHILDREN as is." (setq transient--exitp nil) (setq transient--helpp nil) (setq transient--editp nil) + (setq transient--all-levels-p nil) (setq transient--minibuffer-depth 0) (run-hooks 'transient-exit-hook) (when resume @@ -2293,6 +2455,7 @@ value. Otherwise return CHILDREN as is." (push (list (oref transient--prefix command) transient--layout transient--editp + :transient-suffix (oref transient--prefix transient-suffix) :scope (oref transient--prefix scope)) transient--stack)) @@ -2353,12 +2516,12 @@ value. Otherwise return CHILDREN as is." (concat ", " (apply #'format args))) (args (concat ", " (apply (car args) (cdr args)))) - (t ""))) + (""))) (apply #'message arg args))))) (defun transient--emergency-exit () "Exit the current transient command after an error occurred. -When no transient is active (i.e. when `transient--prefix' is +When no transient is active (i.e., when `transient--prefix' is nil) then do nothing." (transient--debug 'emergency-exit) (when transient--prefix @@ -2369,6 +2532,36 @@ nil) then do nothing." ;;; Pre-Commands +(defun transient--call-pre-command () + (if-let ((fn (transient--get-pre-command this-command))) + (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)) + +(defun transient--get-pre-command (&optional cmd enforce-type) + (or (and (not (eq enforce-type 'non-suffix)) + (lookup-key transient--predicate-map (vector cmd))) + (and (not (eq enforce-type 'suffix)) + (transient--resolve-pre-command + (oref transient--prefix transient-non-suffix) + t)))) + +(defun transient--resolve-pre-command (pre &optional resolve-boolean) + (cond ((booleanp pre) + (if resolve-boolean + (if pre #'transient--do-stay #'transient--do-warn) + pre)) + ((string-match-p "--do-" (symbol-name pre)) pre) + ((let ((sym (intern (format "transient--do-%s" pre)))) + (if (functionp sym) sym pre))))) + (defun transient--do-stay () "Call the command without exporting variables and stay transient." transient--stay) @@ -2409,7 +2602,8 @@ If there is no parent prefix, then behave like `transient--do-exit'." (defun transient--do-leave () "Call the command without exporting variables and exit the transient." - transient--stay) + (transient--stack-zap) + transient--exit) (defun transient--do-push-button () "Call the command represented by the activated button. @@ -2424,26 +2618,35 @@ Use that command's pre-command to determine transient behavior." (posn-point (event-start last-command-event)) (point)) 'command))) - (transient--do-pre-command))) + (transient--call-pre-command))) (defun transient--do-recurse () "Call the transient prefix command, preparing for return to active transient. If there is no parent prefix, then just call the command." - (transient--do-replace)) + (transient--do-stack)) (defun transient--setup-recursion (prefix-obj) (when transient--stack (let ((command (oref prefix-obj command))) (when-let ((suffix-obj (transient-suffix-object command))) - (when (and (slot-boundp suffix-obj 'transient) - (memq (oref suffix-obj transient) - (list t #'transient--do-recurse))) - (oset prefix-obj transient-suffix 'transient--do-return)))))) + (when (memq (if (slot-boundp suffix-obj 'transient) + (oref suffix-obj transient) + (oref transient-current-prefix transient-suffix)) + (list t #'transient--do-recurse)) + (oset prefix-obj transient-suffix t)))))) + +(defun transient--do-stack () + "Call the transient prefix command, stacking the active transient. +Push the active transient to the transient stack." + (transient--export) + (transient--stack-push) + (setq transient--exitp 'replace) + transient--exit) (defun transient--do-replace () - "Call the transient prefix command, replacing the active transient." + "Call the transient prefix command, replacing the active transient. +Do not push the active transient to the transient stack." (transient--export) - (transient--stack-push) (setq transient--exitp 'replace) transient--exit) @@ -2462,7 +2665,9 @@ If there is no parent prefix, then just call the command." (setq transient--editp nil) (transient-setup) transient--stay) - (t transient--exit))) + (prefix-arg + transient--stay) + (transient--exit))) (defun transient--do-quit-all () "Exit all transients without saving the transient stack." @@ -2474,7 +2679,7 @@ If there is no parent prefix, then just call the command." In that case behave like `transient--do-stay', otherwise similar to `transient--do-warn'." (unless transient-enable-popup-navigation - (setq this-command 'transient-popup-navigation-help)) + (setq this-command 'transient-inhibit-move)) transient--stay) (defun transient--do-minus () @@ -2485,22 +2690,27 @@ prefix argument and pivot to `transient-update'." (setq this-command 'transient-update)) transient--stay) -(put 'transient--do-stay 'transient-color 'transient-red) -(put 'transient--do-noop 'transient-color 'transient-red) -(put 'transient--do-warn 'transient-color 'transient-red) -(put 'transient--do-warn-inapt 'transient-color 'transient-red) -(put 'transient--do-call 'transient-color 'transient-red) -(put 'transient--do-return 'transient-color 'transient-purple) -(put 'transient--do-exit 'transient-color 'transient-blue) -(put 'transient--do-recurse 'transient-color 'transient-red) -(put 'transient--do-replace 'transient-color 'transient-blue) -(put 'transient--do-suspend 'transient-color 'transient-blue) -(put 'transient--do-quit-one 'transient-color 'transient-blue) -(put 'transient--do-quit-all 'transient-color 'transient-blue) -(put 'transient--do-move 'transient-color 'transient-red) -(put 'transient--do-minus 'transient-color 'transient-red) +(put 'transient--do-stay 'transient-face 'transient-key-stay) +(put 'transient--do-noop 'transient-face 'transient-key-noop) +(put 'transient--do-warn 'transient-face 'transient-key-noop) +(put 'transient--do-warn-inapt 'transient-face 'transient-key-noop) +(put 'transient--do-call 'transient-face 'transient-key-stay) +(put 'transient--do-return 'transient-face 'transient-key-return) +(put 'transient--do-exit 'transient-face 'transient-key-exit) +(put 'transient--do-leave 'transient-face 'transient-key-exit) + +(put 'transient--do-recurse 'transient-face 'transient-key-stay) +(put 'transient--do-stack 'transient-face 'transient-key-stay) +(put 'transient--do-replace 'transient-face 'transient-key-exit) +(put 'transient--do-suspend 'transient-face 'transient-key-exit) + +(put 'transient--do-quit-one 'transient-face 'transient-key-return) +(put 'transient--do-quit-all 'transient-face 'transient-key-exit) +(put 'transient--do-move 'transient-face 'transient-key-stay) +(put 'transient--do-minus 'transient-face 'transient-key-stay) ;;; Commands +;;;; Noop (defun transient-noop () "Do nothing at all." @@ -2539,27 +2749,23 @@ prefix argument and pivot to `transient-update'." (other-window 1) (display-warning 'transient "Inconsistent transient state detected. This should never happen. -Please open an issue and post the shown command log. -This is a heisenbug, so any additional details might help. -Thanks!" :error))) +Please open an issue and post the shown command log." :error))) -(defun transient-toggle-common () - "Toggle whether common commands are always shown." +(defun transient-inhibit-move () + "Warn the user that popup navigation is disabled." (interactive) - (setq transient-show-common-commands (not transient-show-common-commands))) + (message "To enable use of `%s', please customize `%s'" + this-original-command + 'transient-enable-popup-navigation)) -(defun transient-suspend () - "Suspend the current transient. -It can later be resumed using `transient-resume' while no other -transient is active." - (interactive)) +;;;; Core (defun transient-quit-all () "Exit all transients without saving the transient stack." (interactive)) (defun transient-quit-one () - "Exit the current transients, possibly returning to the previous." + "Exit the current transients, returning to outer transient, if any." (interactive)) (defun transient-quit-seq () @@ -2569,17 +2775,48 @@ transient is active." (defun transient-update () "Redraw the transient's state in the popup buffer." (interactive) - (when (equal this-original-command 'negative-argument) - (setq prefix-arg current-prefix-arg))) + (setq prefix-arg current-prefix-arg)) (defun transient-show () "Show the transient's state in the popup buffer." (interactive) (setq transient--showp t)) -(defvar-local transient--restore-winconf nil) +(defun transient-push-button () + "Invoke the suffix command represented by this button." + (interactive)) -(defvar transient-resume-mode) +;;;; Suspend + +(defun transient-suspend () + "Suspend the current transient. +It can later be resumed using `transient-resume', while no other +transient is active." + (interactive)) + +(define-minor-mode transient-resume-mode + "Auxiliary minor-mode used to resume a transient after viewing help.") + +(defun transient-resume () + "Resume a previously suspended stack of transients." + (interactive) + (cond (transient--stack + (let ((winconf transient--restore-winconf)) + (kill-local-variable 'transient--restore-winconf) + (when transient-resume-mode + (transient-resume-mode -1) + (quit-window)) + (when winconf + (set-window-configuration winconf))) + (transient--stack-pop)) + (transient-resume-mode + (kill-local-variable 'transient--restore-winconf) + (transient-resume-mode -1) + (quit-window)) + (t + (message "No suspended transient command")))) + +;;;; Help (defun transient-help (&optional interactive) "Show help for the active transient or one of its suffixes.\n\n(fn)" @@ -2596,12 +2833,15 @@ transient is active." transient--prefix (or (transient-suffix-object) this-original-command))) - (setq transient--restore-winconf winconf)) + (setq-local transient--restore-winconf winconf)) (fit-window-to-buffer nil (frame-height) (window-height)) (transient-resume-mode) - (message "Type \"q\" to resume transient command.") + (message (substitute-command-keys + "Type \\`q' to resume transient command.")) t)))) +;;;; Level + (defun transient-set-level (&optional command level) "Set the level of the transient or one of its suffix commands." (interactive @@ -2613,10 +2853,9 @@ transient is active." (list command (let ((keys (this-single-command-raw-keys))) (and (lookup-key transient--transient-map keys) - (string-to-number - (let ((transient--active-infix - (transient-suffix-object command))) - (transient--show) + (progn + (transient--show) + (string-to-number (transient--read-number-N (format "Set level for `%s': " command) nil nil (not (eq command prefix))))))))))) @@ -2627,32 +2866,64 @@ transient is active." (level (let* ((prefix (oref transient--prefix command)) (alist (alist-get prefix transient-levels)) - (sym command)) - (if (eq command prefix) - (progn (oset transient--prefix level level) - (setq sym t)) - (oset (transient-suffix-object command) level level)) - (setf (alist-get sym alist) level) + (akey command)) + (cond ((eq command prefix) + (oset transient--prefix level level) + (setq akey t)) + (t + (oset (transient-suffix-object command) level level) + (when (cdr (cl-remove-if-not (lambda (obj) + (eq (oref obj command) command)) + transient--suffixes)) + (setq akey (cons command (this-command-keys)))))) + (setf (alist-get akey alist) level) (setf (alist-get prefix transient-levels) alist)) (transient-save-levels) (transient--show)) (t (transient-undefined)))) +(transient-define-suffix transient-toggle-level-limit () + "Toggle whether to temporarily displayed suffixes on all levels." + :description + (lambda () + (cond + ((= transient-default-level transient--max-level) + "Always displaying all levels") + (transient--all-levels-p + (format "Hide suffix %s" + (propertize + (format "levels > %s" (oref (transient-prefix-object) level)) + 'face 'transient-higher-level))) + ("Show all suffix levels"))) + :inapt-if (lambda () (= transient-default-level transient--max-level)) + :transient t + (interactive) + (setq transient--all-levels-p (not transient--all-levels-p)) + (setq transient--refreshp t)) + +;;;; Value + (defun transient-set () - "Save the value of the active transient for this Emacs session." + "Set active transient's value for this Emacs session." (interactive) - (transient-set-value (or transient--prefix transient-current-prefix))) + (transient-set-value (transient-prefix-object))) + +(defalias 'transient-set-and-exit 'transient-set + "Set active transient's value for this Emacs session and exit.") (defun transient-save () - "Save the value of the active transient persistenly across Emacs sessions." + "Save active transient's value for this and future Emacs sessions." (interactive) - (transient-save-value (or transient--prefix transient-current-prefix))) + (transient-save-value (transient-prefix-object))) + +(defalias 'transient-save-and-exit 'transient-save + "Save active transient's value for this and future Emacs sessions and exit.") (defun transient-reset () "Clear the set and saved values of the active transient." (interactive) - (transient-reset-value (or transient--prefix transient-current-prefix))) + (transient-reset-value (transient-prefix-object))) (defun transient-history-next () "Switch to the next value used for the active transient." @@ -2679,44 +2950,36 @@ transient is active." (oset obj value (nth pos hst)) (mapc #'transient-init-value transient--suffixes)))) -(defun transient-scroll-up (&optional arg) - "Scroll text of transient popup window upward ARG lines. -If ARG is nil scroll near full screen. This is a wrapper -around `scroll-up-command' (which see)." - (interactive "^P") - (with-selected-window transient--window - (scroll-up-command arg))) - -(defun transient-scroll-down (&optional arg) - "Scroll text of transient popup window down ARG lines. -If ARG is nil scroll near full screen. This is a wrapper -around `scroll-down-command' (which see)." - (interactive "^P") - (with-selected-window transient--window - (scroll-down-command arg))) +;;;; Auxiliary -(defun transient-push-button () - "Invoke the suffix command represented by this button." - (interactive)) +(defun transient-toggle-common () + "Toggle whether common commands are permanently shown." + (interactive) + (setq transient-show-common-commands (not transient-show-common-commands))) -(defun transient-resume () - "Resume a previously suspended stack of transients." +(defun transient-toggle-debug () + "Toggle debugging statements for transient commands." (interactive) - (cond (transient--stack - (let ((winconf transient--restore-winconf)) - (kill-local-variable 'transient--restore-winconf) - (when transient-resume-mode - (transient-resume-mode -1) - (quit-window)) - (when winconf - (set-window-configuration winconf))) - (transient--stack-pop)) - (transient-resume-mode - (kill-local-variable 'transient--restore-winconf) - (transient-resume-mode -1) - (quit-window)) - (t - (message "No suspended transient command")))) + (setq transient--debug (not transient--debug)) + (message "Debugging transient %s" + (if transient--debug "enabled" "disabled"))) + +(transient-define-suffix transient-echo-arguments (arguments) + "Show the transient's active ARGUMENTS in the echo area. +Intended for use in prefixes used for demonstration purposes, +such as when suggesting a new feature or reporting an issue." + :transient t + :description "Echo arguments" + :key "x" + (interactive (list (transient-args transient-current-command))) + (message "%s: %s" + (key-description (this-command-keys)) + (mapconcat (lambda (arg) + (propertize (if (string-match-p " " arg) + (format "%S" arg) + arg) + 'face 'transient-argument)) + arguments " "))) ;;; Value ;;;; Init @@ -2822,28 +3085,18 @@ user using the reader specified by the `reader' slot (using the `transient-infix' method described below). For some infix classes the value is changed without reading -anything in the minibuffer, i.e. the mere act of invoking the +anything in the minibuffer, i.e., the mere act of invoking the infix command determines what the new value should be, based on the previous value.") (cl-defmethod transient-infix-read :around ((obj transient-infix)) - "Highlight the infix in the popup buffer. + "Refresh the transient buffer buffer calling the next method. -This also wraps the call to `cl-call-next-method' with two -macros. - -`transient--with-suspended-override' is necessary to allow -reading user input using the minibuffer. - -`transient--with-emergency-exit' arranges for the transient to -be exited in case of an error because otherwise Emacs would get -stuck in an inconsistent state, which might make it necessary to -kill it from the outside. - -If you replace this method, then you must make sure to always use -the latter macro and most likely also the former." - (let ((transient--active-infix obj)) - (transient--show)) +Also wrap `cl-call-next-method' with two macros: +- `transient--with-suspended-override' allows use of minibuffer. +- `transient--with-emergency-exit' arranges for the transient to + be exited in case of an error." + (transient--show) (transient--with-emergency-exit (transient--with-suspended-override (cl-call-next-method obj)))) @@ -2861,7 +3114,7 @@ the lack of history, for example. Only for very simple classes that toggle or cycle through a very limited number of possible values should you replace this with a -simple method that does not handle history. (E.g. for a command +simple method that does not handle history. (E.g., for a command line switch the only possible values are \"use it\" and \"don't use it\", in which case it is pointless to preserve history.)" (with-slots (value multi-value always-read allow-empty choices) obj @@ -2872,6 +3125,7 @@ it\", in which case it is pointless to preserve history.)" (oset obj value nil) (let* ((enable-recursive-minibuffers t) (reader (oref obj reader)) + (choices (if (functionp choices) (funcall choices) choices)) (prompt (transient-prompt obj)) (value (if multi-value (mapconcat #'identity value ",") value)) (history-key (or (oref obj history-key) @@ -2894,7 +3148,7 @@ it\", in which case it is pointless to preserve history.)" initial-input history)) (choices (completing-read prompt choices nil t initial-input history)) - (t (read-string prompt initial-input history))))) + ((read-string prompt initial-input history))))) (cond ((and (equal value "") (not allow-empty)) (setq value nil)) ((and (equal value "\"\"") allow-empty) @@ -2926,7 +3180,7 @@ The last value is \"don't use any of these switches\"." Use this if you want to share an infix's history with a regular stand-alone command." (cl-letf (((symbol-function #'transient--show) #'ignore)) - (transient-infix-read (get command 'transient--suffix)))) + (transient-infix-read (transient--suffix-prototype command)))) ;;;; Readers @@ -3017,8 +3271,6 @@ prompt." ;;;; Set -(defvar transient--unset-incompatible t) - (cl-defgeneric transient-infix-set (obj value) "Set the value of infix object OBJ to value.") @@ -3026,29 +3278,32 @@ prompt." "Set the value of infix object OBJ to value." (oset obj value value)) -(cl-defmethod transient-infix-set :around ((obj transient-argument) value) +(cl-defmethod transient-infix-set :after ((obj transient-argument) value) "Unset incompatible infix arguments." - (let ((arg (if (slot-boundp obj 'argument) - (oref obj argument) - (oref obj argument-regexp)))) - (if-let ((sic (and value arg transient--unset-incompatible)) - (spec (oref transient--prefix incompatible)) - (incomp (cl-mapcan (lambda (rule) - (and (member arg rule) - (remove arg rule))) - spec))) - (progn - (cl-call-next-method obj value) - (dolist (arg incomp) - (when-let ((obj (cl-find-if - (lambda (obj) - (and (slot-exists-p obj 'argument) - (slot-boundp obj 'argument) - (equal (oref obj argument) arg))) - transient--suffixes))) - (let ((transient--unset-incompatible nil)) - (transient-infix-set obj nil))))) - (cl-call-next-method obj value)))) + (when-let* ((--- value) + (val (transient-infix-value obj)) + (arg (if (slot-boundp obj 'argument) + (oref obj argument) + (oref obj argument-format))) + (spec (oref transient--prefix incompatible)) + (filter (lambda (x rule) + (and (member x rule) + (remove x rule)))) + (incomp (nconc + (cl-mapcan (apply-partially filter arg) spec) + (and (not (equal val arg)) + (cl-mapcan (apply-partially filter val) spec))))) + (dolist (obj transient--suffixes) + (when-let* ((--- (cl-typep obj 'transient-argument)) + (val (transient-infix-value obj)) + (arg (if (slot-boundp obj 'argument) + (oref obj argument) + (oref obj argument-format))) + (--- (if (equal val arg) + (member arg incomp) + (or (member val incomp) + (member arg incomp))))) + (transient-infix-set obj nil))))) (cl-defgeneric transient-set-value (obj) "Set the value of the transient prefix OBJ.") @@ -3111,11 +3366,11 @@ the set, saved or default value for PREFIX." (defun transient--get-wrapped-value (obj) (and-let* ((value (transient-infix-value obj))) - (cl-ecase (and (slot-exists-p obj 'multi-value) - (oref obj multi-value)) - ((nil) (list value)) - ((t rest) (list value)) - (repeat value)))) + (pcase-exhaustive (and (slot-exists-p obj 'multi-value) + (oref obj multi-value)) + ('nil (list value)) + ((or 't 'rest) (list value)) + ('repeat value)))) (cl-defgeneric transient-infix-value (obj) "Return the value of the suffix object OBJ. @@ -3150,17 +3405,17 @@ does nothing." nil) "Return ARGUMENT and VALUE as a unit or nil if the latter is nil." (and-let* ((value (oref obj value))) (let ((arg (oref obj argument))) - (cl-ecase (oref obj multi-value) - ((nil) (concat arg value)) - ((t rest) (cons arg value)) - (repeat (mapcar (lambda (v) (concat arg v)) value)))))) + (pcase-exhaustive (oref obj multi-value) + ('nil (concat arg value)) + ((or 't 'rest) (cons arg value)) + ('repeat (mapcar (lambda (v) (concat arg v)) value)))))) (cl-defmethod transient-infix-value ((_ transient-variable)) "Return nil, which means \"no value\". Setting the value of a variable is done by, well, setting the -value of the variable. I.e. this is a side-effect and does not -contribute to the value of the transient." +value of the variable. I.e., this is a side-effect and does +not contribute to the value of the transient." nil) ;;;; Utilities @@ -3242,12 +3497,13 @@ have a history of their own.") (list (propertize (oref suffix key) 'face 'transient-key))))) transient--suffixes) #'string<) - (propertize "|" 'face 'transient-unreachable-key)))))) + (propertize "|" 'face 'transient-delimiter)))))) (defun transient--show () (transient--timer-cancel) (setq transient--showp t) - (let ((buf (get-buffer-create transient--buffer-name)) + (let ((transient--shadowed-buffer (current-buffer)) + (buf (get-buffer-create transient--buffer-name)) (focus nil)) (with-current-buffer buf (when transient-enable-popup-navigation @@ -3260,9 +3516,11 @@ have a history of their own.") (when (bound-and-true-p tab-line-format) (setq tab-line-format nil)) (setq header-line-format nil) - (setq mode-line-format (if (eq transient-mode-line-format 'line) - nil - transient-mode-line-format)) + (setq mode-line-format + (if (or (natnump transient-mode-line-format) + (eq transient-mode-line-format 'line)) + nil + transient-mode-line-format)) (setq mode-line-buffer-identification (symbol-name (oref transient--prefix command))) (if transient-enable-popup-navigation @@ -3273,16 +3531,8 @@ have a history of their own.") (transient--insert-groups) (when (or transient--helpp transient--editp) (transient--insert-help)) - (when (and (eq transient-mode-line-format 'line) - window-system) - (let ((face - (if-let ((f (and (transient--semantic-coloring-p) - (transient--prefix-color transient--prefix)))) - `(,@(and (>= emacs-major-version 27) '(:extend t)) - :background ,(face-foreground f)) - 'transient-separator))) - (insert (propertize "__" 'face face 'display '(space :height (1)))) - (insert (propertize "\n" 'face face 'line-height t)))) + (when-let ((line (transient--separator-line))) + (insert line)) (when transient-force-fixed-pitch (transient--force-fixed-pitch))) (unless (window-live-p transient--window) @@ -3304,11 +3554,31 @@ have a history of their own.") (fit-window-to-buffer window nil (window-height window)) (fit-window-to-buffer window nil 1)))) +(defun transient--separator-line () + (and-let* ((height (cond ((not window-system) nil) + ((natnump transient-mode-line-format) + transient-mode-line-format) + ((eq transient-mode-line-format 'line) 1))) + (face `(,@(and (>= emacs-major-version 27) '(:extend t)) + :background + ,(or (face-foreground (transient--key-face nil 'non-suffix) + nil t) + "#gray60")))) + (concat (propertize "__" 'face face 'display `(space :height (,height))) + (propertize "\n" 'face face 'line-height t)))) + +(defmacro transient-with-shadowed-buffer (&rest body) + "While in the transient buffer, temporarly make the shadowed buffer current." + (declare (indent 0) (debug t)) + `(with-current-buffer (or transient--shadowed-buffer (current-buffer)) + ,@body)) + (defun transient--insert-groups () (let ((groups (cl-mapcan (lambda (group) (let ((hide (oref group hide))) (and (not (and (functionp hide) - (funcall hide))) + (transient-with-shadowed-buffer + (funcall hide)))) (list group)))) transient--layout)) group) @@ -3324,23 +3594,25 @@ have a history of their own.") (cl-defmethod transient--insert-group :around ((group transient-group)) "Insert GROUP's description, if any." - (when-let ((desc (transient-format-description group))) + (when-let ((desc (transient-with-shadowed-buffer + (transient-format-description group)))) (insert desc ?\n)) (let ((transient--max-group-level - (max (oref group level) transient--max-group-level))) + (max (oref group level) transient--max-group-level)) + (transient--pending-group group)) (cl-call-next-method group))) (cl-defmethod transient--insert-group ((group transient-row)) (transient--maybe-pad-keys group) (dolist (suffix (oref group suffixes)) - (insert (transient-format suffix)) + (insert (transient-with-shadowed-buffer (transient-format suffix))) (insert " ")) (insert ?\n)) (cl-defmethod transient--insert-group ((group transient-column)) (transient--maybe-pad-keys group) (dolist (suffix (oref group suffixes)) - (let ((str (transient-format suffix))) + (let ((str (transient-with-shadowed-buffer (transient-format suffix)))) (insert str) (unless (string-match-p ".\n\\'" str) (insert ?\n))))) @@ -3350,10 +3622,11 @@ have a history of their own.") (mapcar (lambda (column) (transient--maybe-pad-keys column group) - (let ((rows (mapcar #'transient-format (oref column suffixes)))) - (when-let ((desc (transient-format-description column))) - (push desc rows)) - (flatten-tree rows))) + (transient-with-shadowed-buffer + (let ((rows (mapcar #'transient-format (oref column suffixes)))) + (when-let ((desc (transient-format-description column))) + (push desc rows)) + (flatten-tree rows)))) (oref group suffixes))) (vp (or (oref transient--prefix variable-pitch) transient-align-variable-pitch)) @@ -3393,15 +3666,6 @@ have a history of their own.") (when (= c (1- cs)) (insert ?\n)))))))) -(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)))))) - (cl-defmethod transient--insert-group ((group transient-subgroups)) (let* ((subgroups (oref group suffixes)) (n (length subgroups))) @@ -3432,36 +3696,31 @@ making `transient--original-buffer' current.") "Return a string containing just the ARG character." (char-to-string arg)) -(cl-defmethod transient-format :around ((obj transient-infix)) - "When reading user input for this infix, then highlight it." +(cl-defmethod transient-format :around ((obj transient-suffix)) + "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 +as a button." (let ((str (cl-call-next-method obj))) - (when (eq obj transient--active-infix) - (setq str (concat str "\n")) - (add-face-text-property - (if (eq this-command 'transient-set-level) 3 0) - (length str) - 'transient-active-infix nil str)) + (when (and (cl-typep obj 'transient-infix) + (eq (oref obj command) this-original-command) + (active-minibuffer-window)) + (setq str (transient--add-face str 'transient-active-infix))) + (when transient--editp + (setq str (concat (let ((level (oref obj level))) + (propertize (format " %s " level) + 'face (if (transient--use-level-p level t) + '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 + 'command (oref obj command)))) str)) -(cl-defmethod transient-format :around ((obj transient-suffix)) - "When edit-mode is enabled, then prepend the level information. -Optional support for popup buttons is also implemented here." - (let ((str (concat - (and transient--editp - (let ((level (oref obj level))) - (propertize (format " %s " level) - 'face (if (transient--use-level-p level t) - 'transient-enabled-suffix - 'transient-disabled-suffix)))) - (cl-call-next-method obj)))) - (when (oref obj inapt) - (add-face-text-property 0 (length str) 'transient-inapt-suffix nil str)) - (if transient-enable-popup-navigation - (make-text-button str nil - 'type 'transient - 'command (oref obj command)) - str))) - (cl-defmethod transient-format ((obj transient-infix)) "Return a string generated using OBJ's `format'. %k is formatted using `transient-format-key'. @@ -3483,10 +3742,19 @@ Optional support for popup buttons is also implemented here." (cl-defgeneric transient-format-key (obj) "Format OBJ's `key' for display and return the result.") +(cl-defmethod transient-format-key :around ((obj transient-suffix)) + "Add `transient-inapt-suffix' face if suffix is inapt." + (let ((str (cl-call-next-method))) + (if (oref obj inapt) + (transient--add-face str 'transient-inapt-suffix) + str))) + (cl-defmethod transient-format-key ((obj transient-suffix)) "Format OBJ's `key' for display and return the result." - (let ((key (oref obj key)) - (cmd (oref obj command))) + (let ((key (if (slot-boundp obj 'key) (oref obj key) "")) + (cmd (and (slot-boundp obj 'command) (oref obj command)))) + (when-let ((width (oref transient--pending-group pad-keys))) + (setq key (truncate-string-to-width key width nil ?\s))) (if transient--redisplay-key (let ((len (length transient--redisplay-key)) (seq (cl-coerce (edmacro-parse-keys key t) 'list))) @@ -3503,7 +3771,7 @@ Optional support for popup buttons is also implemented here." (setq pre (string-replace "TAB" "C-i" pre)) (setq suf (string-replace "RET" "C-m" suf)) (setq suf (string-replace "TAB" "C-i" suf)) - ;; We use e.g. "-k" instead of the more correct "- k", + ;; We use e.g., "-k" instead of the more correct "- k", ;; because the former is prettier. If we did that in ;; the definition, then we want to drop the space that ;; is reinserted above. False-positives are possible @@ -3513,33 +3781,27 @@ Optional support for popup buttons is also implemented here." (setq suf (string-replace " " "" suf))) (concat (propertize pre 'face 'transient-unreachable-key) (and (string-prefix-p (concat pre " ") key) " ") - (transient--colorize-key suf cmd) + (propertize suf 'face (transient--key-face cmd)) (save-excursion (and (string-match " +\\'" key) (propertize (match-string 0 key) 'face 'fixed-pitch)))))) ((transient--lookup-key transient-sticky-map (kbd key)) - (transient--colorize-key key cmd)) + (propertize key 'face (transient--key-face cmd))) (t (propertize key 'face 'transient-unreachable-key)))) - (transient--colorize-key key cmd)))) - -(defun transient--colorize-key (key command) - (propertize key 'face - (or (and (transient--semantic-coloring-p) - (transient--suffix-color command)) - 'transient-key))) + (propertize key 'face (transient--key-face cmd))))) (cl-defmethod transient-format-key :around ((obj transient-argument)) + "Handle `transient-highlight-mismatched-keys'." (let ((key (cl-call-next-method obj))) - (cond ((not transient-highlight-mismatched-keys)) - ((not (slot-boundp obj 'shortarg)) - (add-face-text-property - 0 (length key) 'transient-nonstandard-key nil key)) - ((not (string-equal key (oref obj shortarg))) - (add-face-text-property - 0 (length key) 'transient-mismatched-key nil key))) - key)) + (cond + ((not transient-highlight-mismatched-keys) key) + ((not (slot-boundp obj 'shortarg)) + (transient--add-face key 'transient-nonstandard-key)) + ((not (string-equal key (oref obj shortarg))) + (transient--add-face key 'transient-mismatched-key)) + (key)))) (cl-defgeneric transient-format-description (obj) "Format OBJ's `description' for display and return the result.") @@ -3548,10 +3810,14 @@ Optional support for popup buttons is also implemented here." "The `description' slot may be a function, in which case that is called inside the correct buffer (see `transient--insert-group') and its value is returned to the caller." - (and-let* ((desc (oref obj description))) - (if (functionp desc) - (with-current-buffer transient--original-buffer - (funcall desc)) + (and-let* ((desc (oref obj description)) + (desc (if (functionp desc) + (if (= (car (func-arity desc)) 1) + (funcall desc obj) + (funcall desc)) + desc))) + (if-let* ((face (transient--get-face obj 'face))) + (transient--add-face desc face t) desc))) (cl-defmethod transient-format-description ((obj transient-group)) @@ -3573,16 +3839,19 @@ If the OBJ's `key' is currently unreachable, then apply the face (funcall (oref transient--prefix suffix-description) obj)) (propertize "(BUG: no description)" 'face 'error)))) - (cond ((transient--key-unreachable-p obj) - (propertize desc 'face 'transient-unreachable)) - ((and transient-highlight-higher-levels - (> (max (oref obj level) transient--max-group-level) - transient--default-prefix-level)) - (add-face-text-property - 0 (length desc) 'transient-higher-level nil desc) - desc) - (t - desc)))) + (when (if transient--all-levels-p + (> (oref obj level) transient--default-prefix-level) + (and transient-highlight-higher-levels + (> (max (oref obj level) transient--max-group-level) + transient--default-prefix-level))) + (setq desc (transient--add-face desc 'transient-higher-level))) + (when-let ((inapt-face (and (oref obj inapt) + (transient--get-face obj 'inapt-face)))) + (setq desc (transient--add-face desc inapt-face))) + (when (and (slot-boundp obj 'key) + (transient--key-unreachable-p obj)) + (setq desc (transient--add-face desc 'transient-unreachable))) + desc)) (cl-defgeneric transient-format-value (obj) "Format OBJ's value for display and return the result.") @@ -3596,24 +3865,32 @@ If the OBJ's `key' is currently unreachable, then apply the face (cl-defmethod transient-format-value ((obj transient-option)) (let ((argument (oref obj argument))) (if-let ((value (oref obj value))) - (propertize - (cl-ecase (oref obj multi-value) - ((nil) (concat argument value)) - ((t rest) (concat argument - (and (not (string-suffix-p " " argument)) " ") - (mapconcat #'prin1-to-string value " "))) - (repeat (mapconcat (lambda (v) (concat argument v)) value " "))) - 'face 'transient-value) - (propertize argument 'face 'transient-inactive-value)))) + (pcase-exhaustive (oref obj multi-value) + ('nil + (concat (propertize argument 'face 'transient-argument) + (propertize value 'face 'transient-value))) + ((or 't 'rest) + (concat (propertize (if (string-suffix-p " " argument) + argument + (concat argument " ")) + 'face 'transient-argument) + (propertize (mapconcat #'prin1-to-string value " ") + 'face 'transient-value))) + ('repeat + (mapconcat (lambda (value) + (concat (propertize argument 'face 'transient-argument) + (propertize value 'face 'transient-value))) + value " "))) + (propertize argument 'face 'transient-inactive-argument)))) (cl-defmethod transient-format-value ((obj transient-switches)) (with-slots (value argument-format choices) obj (format (propertize argument-format 'face (if value - 'transient-value - 'transient-inactive-value)) - (concat - (propertize "[" 'face 'transient-inactive-value) + 'transient-argument + 'transient-inactive-argument)) + (format + (propertize "[%s]" 'face 'transient-delimiter) (mapconcat (lambda (choice) (propertize choice 'face @@ -3621,8 +3898,30 @@ If the OBJ's `key' is currently unreachable, then apply the face 'transient-value 'transient-inactive-value))) choices - (propertize "|" 'face 'transient-inactive-value)) - (propertize "]" 'face 'transient-inactive-value))))) + (propertize "|" 'face 'transient-delimiter)))))) + +(defun transient--add-face (string face &optional append beg end) + (let ((str (copy-sequence string))) + (add-face-text-property (or beg 0) (or end (length str)) face append str) + str)) + +(defun transient--get-face (obj slot) + (and-let* ((! (slot-exists-p obj slot)) + (! (slot-boundp obj slot)) + (face (slot-value obj slot))) + (if (and (not (facep face)) + (functionp face)) + (funcall face) + face))) + +(defun transient--key-face (&optional cmd enforce-type) + (or (and transient-semantic-coloring + (not transient--helpp) + (not transient--editp) + (or (and cmd (get cmd 'transient-face)) + (get (transient--get-pre-command cmd enforce-type) + 'transient-face))) + (if cmd 'transient-key 'transient-key-noop))) (defun transient--key-unreachable-p (obj) (and transient--redisplay-key @@ -3637,19 +3936,24 @@ If the OBJ's `key' is currently unreachable, then apply the face (and val (not (integerp val)) val))) (defun transient--maybe-pad-keys (group &optional parent) - (when-let ((pad (if (slot-boundp group 'pad-keys) - (oref group pad-keys) - (and parent - (slot-boundp parent 'pad-keys) - (oref parent pad-keys))))) - (let ((width (apply #'max - (cons (if (integerp pad) pad 0) - (mapcar (lambda (suffix) - (length (oref suffix key))) - (oref group suffixes)))))) - (dolist (suffix (oref group suffixes)) - (oset suffix key - (truncate-string-to-width (oref suffix key) width nil ?\s)))))) + (when-let ((pad (or (oref group pad-keys) + (and parent (oref parent pad-keys))))) + (oset group pad-keys + (apply #'max (cons (if (integerp pad) pad 0) + (seq-keep (lambda (suffix) + (and (eieio-object-p suffix) + (slot-boundp suffix 'key) + (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-command-summary-or-name (obj) "Return the summary or name of the command represented by OBJ. @@ -3677,7 +3981,7 @@ if non-nil, else show the `man-page' if non-nil, else use (cond (show-help (funcall show-help obj)) (info-manual (transient--show-manual info-manual)) (man-page (transient--show-manpage man-page)) - (t (transient--describe-function command))))) + ((transient--describe-function command))))) (cl-defmethod transient-show-help ((obj transient-suffix)) "Call `show-help' if non-nil, else use `describe-function'. @@ -3691,9 +3995,9 @@ prefix method." 'transient--prefix))) (and prefix (not (eq (oref transient--prefix command) this-command)) (prog1 t (transient-show-help prefix))))) - (t (if-let ((show-help (oref obj show-help))) - (funcall show-help obj) - (transient--describe-function this-command))))) + ((if-let ((show-help (oref obj show-help))) + (funcall show-help obj) + (transient--describe-function this-command))))) (cl-defmethod transient-show-help ((obj transient-infix)) "Call `show-help' if non-nil, else show the `man-page' @@ -3713,7 +4017,7 @@ manpage, then try to jump to the correct location." (transient--describe-function cmd)) (defun transient--describe-function (fn) - (describe-function (if (symbolp fn) fn 'transient--anonymous-infix-argument)) + (describe-function fn) (unless (derived-mode-p 'help-mode) (when-let* ((buf (get-buffer "*Help*")) (win (or (and buf (get-buffer-window buf)) @@ -3723,21 +4027,6 @@ manpage, then try to jump to the correct location." (window-list))))) (select-window win)))) -(defun transient--anonymous-infix-argument () - "Cannot show any documentation for this anonymous infix command. - -The infix command in question was defined anonymously, i.e., -it was define when the prefix command that it belongs to was -defined, which means that it gets no docstring and also that -no symbol is bound to it. - -When you request help for an infix command, then we usually -show the respective man-page and jump to the location where -the respective argument is being described. - -Because the containing prefix command does not specify any -man-page, we cannot do that in this case. Sorry about that.") - (defun transient--show-manual (manual) (info manual)) @@ -3830,37 +4119,23 @@ Suffixes on levels %s and %s are unavailable.\n" (propertize (format ">=%s" (1+ level)) 'face 'transient-disabled-suffix)))))) -(defvar-keymap transient-resume-mode-map - :doc "Keymap for `transient-resume-mode'. - -This keymap remaps every command that would usually just quit the -documentation buffer to `transient-resume', which additionally -resumes the suspended transient." - " " #'transient-resume - " " #'transient-resume - " " #'transient-resume) - -(define-minor-mode transient-resume-mode - "Auxiliary minor-mode used to resume a transient after viewing help.") - -(defun transient-toggle-debug () - "Toggle debugging statements for transient commands." - (interactive) - (setq transient--debug (not transient--debug)) - (message "Debugging transient %s" - (if transient--debug "enabled" "disabled"))) - ;;; Popup Navigation -(defun transient-popup-navigation-help () - "Inform the user how to enable popup navigation commands." - (interactive) - (message "This command is only available if `%s' is non-nil" - 'transient-enable-popup-navigation)) +(defun transient-scroll-up (&optional arg) + "Scroll text of transient popup window upward ARG lines. +If ARG is nil scroll near full screen. This is a wrapper +around `scroll-up-command' (which see)." + (interactive "^P") + (with-selected-window transient--window + (scroll-up-command arg))) -(define-button-type 'transient - 'face nil - 'keymap transient-button-map) +(defun transient-scroll-down (&optional arg) + "Scroll text of transient popup window down ARG lines. +If ARG is nil scroll near full screen. This is a wrapper +around `scroll-down-command' (which see)." + (interactive "^P") + (with-selected-window transient--window + (scroll-down-command arg))) (defun transient-backward-button (n) "Move to the previous button in the transient popup buffer. @@ -3876,6 +4151,10 @@ See `forward-button' for information about N." (with-selected-window transient--window (forward-button n t))) +(define-button-type 'transient + 'face nil + 'keymap transient-button-map) + (defun transient--goto-button (command) (cond ((stringp command) @@ -3953,36 +4232,6 @@ search instead." (select-window transient--original-window) (transient--resume-override)) -;;;; Hydra Color Emulation - -(defun transient--semantic-coloring-p () - (and transient-semantic-coloring - (not transient--helpp) - (not transient--editp))) - -(defun transient--suffix-color (command) - (or (get command 'transient-color) - (get (transient--get-predicate-for command) 'transient-color))) - -(defun transient--prefix-color (command) - (let* ((nonsuf (or (oref command transient-non-suffix) - 'transient--do-warn)) - (nonsuf (if (memq nonsuf '(transient--do-noop transient--do-warn)) - 'disallow - (get nonsuf 'transient-color))) - (suffix (if-let ((pred (oref command transient-suffix))) - (get pred 'transient-color) - (if (eq nonsuf 'transient-red) - 'transient-red - 'transient-blue)))) - (pcase (list suffix nonsuf) - (`(transient-purple ,_) 'transient-purple) - ('(transient-red disallow) 'transient-amaranth) - ('(transient-blue disallow) 'transient-teal) - ('(transient-red transient-red) 'transient-pink) - ('(transient-red transient-blue) 'transient-red) - ('(transient-blue transient-blue) 'transient-blue)))) - ;;;; Edebug (defun transient--edebug-command-p () @@ -4044,7 +4293,7 @@ we stop there." (let ((key (oref obj key))) (cond ((string-equal key "q") "Q") ((string-equal key "Q") "M-q") - (t key)))) + (key)))) (defun transient--force-fixed-pitch () (require 'face-remap) @@ -4079,8 +4328,7 @@ we stop there." (regexp-opt (list "transient-define-prefix" "transient-define-infix" "transient-define-argument" - "transient-define-suffix" - "transient-define-groups") + "transient-define-suffix") t) "\\_>[ \t'(]*" "\\(\\(?:\\sw\\|\\s_\\)+\\)?") -- 2.39.2