Replies: 3 comments 4 replies
-
Answering that will require that I do some experimentation and since I am focused on wrapping things up, you'll have to wait for that until next year. Merry Christmas and Happy New year 🎆 |
Beta Was this translation helpful? Give feedback.
-
Revisiting this, I've made some progress since last, and have come up with a working implementation that doesn't require changes in Transient. The alternative solution mentioned before is also expanded on at the end (which avoids using advice), but I think there is some kind of bug causing it to not fully work. Here's what the current solution looks like, with an additional subclass example to better illustrate usage: ;;; -*- lexical-binding: t; -*-
(require 'cl-macs)
(require 'eieio)
(require 'transient)
(defclass test-suffix (transient-suffix) ())
(defun test-wrapped-function (func)
"Wrap FUNC with wrapper functions from `test-suffix-wrappers'.
The wrappers that are used depend on the current suffix's (as
returned by `transient-suffix-object') class, since
`test-suffix-wrappers' may be extended by subclasses. If a
current suffix is available and its class is a subclass of
`test-suffix', the wrappers from its class will be used;
otherwise, this function returns FUNC as-is."
(if-let* ((suffix (transient-suffix-object))
( (cl-typep suffix test-suffix)))
(seq-reduce (lambda (oldfun wrapper)
(lambda (&rest r) (apply wrapper oldfun r)))
(test-suffix-wrappers suffix)
func)
func))
(defun test-suffix-command-advice (func &rest r)
"Advise FUNC with `test-suffix' wrappers when available.
See `test-wrapped-function'.
FUNC and R are as defined in the `advice-add' :around format."
(interactive
(lambda (spec)
(funcall (test-wrapped-function
(lambda ()
(advice-eval-interactive-spec spec))))))
(apply (test-wrapped-function func) r))
(cl-defmethod initialize-instance :after ((obj test-suffix) &optional _slots)
"Do additional initialization for OBJ.
Add advice to the suffix object's command."
(let* ((sym (oref obj command)))
(advice-add sym :around #'test-suffix-command-advice)))
(cl-defmethod test-suffix-wrappers ((_obj test-suffix))
"Return a list of functions to wrap the suffix command.
Each function should follow the :around `advice-add' format,
expecting to be called like so:
(lambda (&rest r) (apply WRAPPER OLDFUN r))
The first function in the list will be the innermost wrapper."
(list (lambda (func &rest r)
;; For demonstration purposes, set `default-directory' to system root
;; directory.
(let ((default-directory "/"))
(apply func r)))))
(defclass test-suffix-window (test-suffix) ())
(cl-defmethod test-suffix-wrappers ((_obj test-suffix-window))
"Return a list of functions to wrap the suffix command."
(cons (lambda (func &rest r)
(let ((display-buffer-overriding-action
'(display-buffer-use-some-window (inhibit-same-window t))))
(apply func r)))
(cl-call-next-method)))
(transient-define-suffix test-message ()
:class test-suffix
(interactive)
(message "Running from: %s" default-directory))
(transient-define-prefix test-prefix ()
["Commands"
("d" "Send message from defined suffix" test-message :class test-suffix)
("f" "Find file" find-file :class test-suffix)
("F" "Find file (other window)" find-file :class test-suffix-window)]
["Other commands"
:class transient-column
:setup-children
(lambda (&rest _r)
(transient-parse-suffixes
'test-prefix
;; This value can come from another source, like dir-locals of a project.
'(("s" "Send message from a \"per-project\" suffix"
(lambda () (interactive)
(message "Hi! Running from: %s" default-directory))
:class test-suffix))))]) This makes use of advice that selectively wraps commands depending on the return value of The other lead with modifying the Here's a demonstration of the issue: ;;; -*- lexical-binding: t; -*-
(require 'eieio)
(require 'transient)
(defclass test2-suffix (transient-suffix) ())
(cl-defmethod initialize-instance :after ((obj test2-suffix) &optional _slots)
;; All commands using this class should not be able to be called in the menu.
(oset obj command nil))
(transient-define-suffix test2-message ()
:class test2-suffix
(interactive)
(message "Running from: %s" default-directory))
(transient-define-prefix test2-prefix ()
["Commands"
("a" "Message from defined suffix" test2-message)
("b" "Find file with `test2-suffix' class" find-file :class test2-suffix)]
["Other commands"
:class transient-column
:setup-children
(lambda (&rest _r)
(transient-parse-suffixes
'test2-prefix
;; This value can come from another source, like dir-locals of a project.
'(("c" "Send message from an ad-hoc suffix"
(lambda () (interactive)
(message "Hi! Running from: %s" default-directory))
:class test2-suffix))))]) Evaluating this and executing |
Beta Was this translation helpful? Give feedback.
-
I think it help to reformulate the essence of the feature, without overloading that by also describing a possible implementation strategy: Allow advising individual suffix bindings. Once I understood that this is what you want, it became much easier to reason about it. We can break down the problem into two distinct tasks.
You chose to tie the advice to a class. That is unnecessary and pretty much the opposite of what we need. Classes allow specifying common behavior for a new set of commands, but we want to attach different behavior to individual 1 existing commands, individual command bindings even! Using classes works, as you have demonstrated, but only for commands that do not already have a class. If you take a command that was defined using All we need instead is a new suffix slot. To allow any command (including those that have a type assigned to them), that slot has to be added in As for how to activate the advice; while a transient is active, we already advice This requires few changes in Using it in a prefix definition is very easy and takes very little code: (defun demo-default-directory (arg)
(interactive (list default-directory))
(message "arg: %s -- value: %s" arg default-directory))
(defun demo-in-/tmp (fn &rest args)
(let ((default-directory "/tmp/"))
(apply fn args)))
(transient-define-prefix demo ()
:transient-suffix t
[("h" "here" demo-default-directory)
("m" "in /tmp" demo-default-directory :advice demo-in-/tmp)
("b" "in /tmp (both)" demo-default-directory :advice* demo-in-/tmp)]) Note that I added two slots; one is used for the body only, the other for the I don't think this should support using multiple advises. If you need multiple things, then define an advice that does multiple things. If you want to compose multiple advises, using Here is a modified example, that demonstrates that this just works with (defun demo-default-directory (arg)
(interactive (list (read-file-name ": ")))
(message "arg: %s -- value: %s" arg default-directory))
(defun demo-in-/tmp (fn &rest args)
(let ((default-directory "/tmp/")
;; This affects the value returned by `read-file-name-default'
;; if the user exits with the initial minibuf contents intact.
;; In other cases binding this variable as well would be wrong.
;; There is no silver bullet.
(buffer-file-name "/tmp"))
(apply fn args)))
(transient-define-prefix demo ()
[("h" "here" demo-default-directory)
("m" "in /tmp" demo-default-directory :advice demo-in-/tmp)
("b" "in /tmp (both)" demo-default-directory :advice* demo-in-/tmp)]
[:class transient-column
:setup-children
(lambda (&rest _)
(transient-parse-suffixes
'test2-prefix
'(("a" "\"anonymous\""
(lambda (arg)
(interactive (list default-directory))
(message "arg: %s -- value: %s -- cmd: %s"
arg default-directory this-command))
:advice* demo-in-/tmp))))]) I will probably add the new slots to Transient, but want to let it simmer a bit before pushing to main. (I actually did something similar recently in 05c011b (documentation added in fe5fd39), so I recognize the need to allow this sort of thing, but it's advanced, "complicated" behavior, so I want to be a bit careful. Most menu authors won't need it and there is a risk of overwhelming them with even more functionality. Footnotes
|
Beta Was this translation helpful? Give feedback.
-
Hello!
I've been playing around with alternative implementations of a feature for my project that involves modifying the
transient-suffix
command
slot, but I appear to have hit a blocker with figuring out how to actually replace it. I'm wondering now if there is a better path to what I'm trying to do, or maybe if this should be a feature request instead.Background
The intention in this case is to essentially be able to add advice or wrap the command such that behaviors may be changed depending on the suffix class used. For example, I'd like to define a class that - when used - always has the command run in a designated
default-directory
, or manages the compilation buffer associated with the command.In addition to some code deduplication, this would be useful for per-project suffixes. I have an existing implementation that implements management of variables like the ones mentioned, but it relies on a bespoke syntax that expands into something that
transient-parse-suffixes
can read, and necessarily restricts the user to what I've implemented in the syntax (not to mention the overhead of users having to learn an extra DSL). Preferably, this could be done with just Transient syntax.Problem with attempted solutions
Here's a (buggy) proof of concept for what I'm trying to accomplish, simplified from what I've implemented in my
suffix-classes
branch (here; specific commit that adds the classes here):This mostly works, but as noted in one of the comments, one of its problems is that the advice will affect unrelated calls as well if a command like
find-file
is inserted.I had some other iterations where I tried replacing the value of the
command
slot with another function, but for some reason unclear to me the new command would not be not used. If we replaceinitialize-instance
with the commented version and re-evaluate, for instance, none of the suffix commands are run in the expected directory when called from the prefix or with a direct call totest-message
. Completing atest:*
command viaeval-expression
, however, gives the expected correct result.Beta Was this translation helpful? Give feedback.
All reactions