Estoy trabajando en un modo Emacs que le permite controlar Emacs con reconocimiento de voz. Uno de los problemas con los que me he encontrado es que la forma en que Emacs maneja deshacer no coincide con la forma en que esperaría que funcione cuando se controla por voz.
Cuando el usuario habla varias palabras y luego hace una pausa, eso se llama "enunciado". Un enunciado puede consistir en múltiples comandos para que Emacs se ejecute. A menudo, el reconocedor reconoce uno o más comandos dentro de un enunciado incorrectamente. En ese momento quiero poder decir "deshacer" y hacer que Emacs deshaga todas las acciones realizadas por el enunciado, no solo la última acción dentro del enunciado. En otras palabras, quiero que Emacs trate un enunciado como un solo comando en lo que respecta a deshacer, incluso cuando un enunciado consta de múltiples comandos. También me gustaría volver al punto exacto donde estaba antes del enunciado, he notado que Emacs normal deshacer no hace esto.
He configurado Emacs para recibir devoluciones de llamada al principio y al final de cada enunciado, para poder detectar la situación, solo necesito averiguar qué hacer Emacs. Lo ideal sería llamar a algo así (undo-start-collapsing)
y luego, (undo-stop-collapsing)
y todo lo que se haga en el medio se colapsaría mágicamente en un solo registro.
Revisé la documentación y encontré undo-boundary
, pero es lo contrario de lo que quiero: necesito colapsar todas las acciones dentro de un enunciado en un registro de deshacer, no dividirlas. Puedo usar undo-boundary
entre enunciados para asegurarme de que las inserciones se consideren separadas (Emacs por defecto considera que las acciones de inserción consecutivas son una acción hasta cierto límite), pero eso es todo.
Otras complicaciones
- Mi demonio de reconocimiento de voz envía algunos comandos a Emacs mediante la simulación de pulsaciones de teclas X11 y envía algunos a través de
emacsclient -e
esto, si se dice que(undo-collapse &rest ACTIONS)
no hay un lugar central que pueda envolver. - Yo uso
undo-tree
, no estoy seguro si esto hace las cosas más complicadas. Idealmente, una solución funcionaría conundo-tree
el comportamiento normal de deshacer de Emacs. - ¿Qué sucede si uno de los comandos dentro de un enunciado es "deshacer" o "rehacer"? Estoy pensando que podría cambiar la lógica de devolución de llamada para enviarlos siempre a Emacs como expresiones distintas para mantener las cosas más simples, entonces debería manejarse como lo haría si estuviera usando el teclado.
- Objetivo de estiramiento: un enunciado puede contener un comando que cambia la ventana o el búfer activos actualmente. En este caso, está bien tener que decir "deshacer" una vez por separado en cada búfer, no necesito que sea tan elegante. Pero todos los comandos en un solo búfer todavía deberían estar agrupados, así que si digo "do-x do-y do-z switch-buffer do-a do-b do-c", entonces x, y, z deberían ser una deshacer registro en el búfer original y a, b, c debe ser un registro en el búfer conmutado.
¿Hay una forma fácil de hacer esto? AFAICT no hay nada incorporado, pero Emacs es vasto y profundo ...
Actualización: Terminé usando la solución de jhc a continuación con un pequeño código adicional. En el global before-change-hook
, verifico si el búfer que se está cambiando está en una lista global de búferes modificados este enunciado, si no, entra en la lista y undo-collapse-begin
se llama. Luego, al final de la emisión, repito todos los búferes en la lista y llamo undo-collapse-end
. Código a continuación (md- agregado antes de los nombres de funciones para propósitos de espacio de nombres):
(defvar md-utterance-changed-buffers nil)
(defvar-local md-collapse-undo-marker nil)
(defun md-undo-collapse-begin (marker)
"Mark the beginning of a collapsible undo block.
This must be followed with a call to undo-collapse-end with a marker
eq to this one.
Taken from jch's stackoverflow answer here:
http://emacs.stackexchange.com/a/7560/2301
"
(push marker buffer-undo-list))
(defun md-undo-collapse-end (marker)
"Collapse undo history until a matching marker.
Taken from jch's stackoverflow answer here:
http://emacs.stackexchange.com/a/7560/2301"
(cond
((eq (car buffer-undo-list) marker)
(setq buffer-undo-list (cdr buffer-undo-list)))
(t
(let ((l buffer-undo-list))
(while (not (eq (cadr l) marker))
(cond
((null (cdr l))
(error "md-undo-collapse-end with no matching marker"))
((eq (cadr l) nil)
(setf (cdr l) (cddr l)))
(t (setq l (cdr l)))))
;; remove the marker
(setf (cdr l) (cddr l))))))
(defmacro md-with-undo-collapse (&rest body)
"Execute body, then collapse any resulting undo boundaries.
Taken from jch's stackoverflow answer here:
http://emacs.stackexchange.com/a/7560/2301"
(declare (indent 0))
(let ((marker (list 'apply 'identity nil)) ; build a fresh list
(buffer-var (make-symbol "buffer")))
`(let ((,buffer-var (current-buffer)))
(unwind-protect
(progn
(md-undo-collapse-begin ',marker)
,@body)
(with-current-buffer ,buffer-var
(md-undo-collapse-end ',marker))))))
(defun md-check-undo-before-change (beg end)
"When a modification is detected, we push the current buffer
onto a list of buffers modified this utterance."
(unless (or
;; undo itself causes buffer modifications, we
;; don't want to trigger on those
undo-in-progress
;; we only collapse utterances, not general actions
(not md-in-utterance)
;; ignore undo disabled buffers
(eq buffer-undo-list t)
;; ignore read only buffers
buffer-read-only
;; ignore buffers we already marked
(memq (current-buffer) md-utterance-changed-buffers)
;; ignore buffers that have been killed
(not (buffer-name)))
(push (current-buffer) md-utterance-changed-buffers)
(setq md-collapse-undo-marker (list 'apply 'identity nil))
(undo-boundary)
(md-undo-collapse-begin md-collapse-undo-marker)))
(defun md-pre-utterance-undo-setup ()
(setq md-utterance-changed-buffers nil)
(setq md-collapse-undo-marker nil))
(defun md-post-utterance-collapse-undo ()
(unwind-protect
(dolist (i md-utterance-changed-buffers)
;; killed buffers have a name of nil, no point
;; in undoing those
(when (buffer-name i)
(with-current-buffer i
(condition-case nil
(md-undo-collapse-end md-collapse-undo-marker)
(error (message "Couldn't undo in buffer %S" i))))))
(setq md-utterance-changed-buffers nil)
(setq md-collapse-undo-marker nil)))
(defun md-force-collapse-undo ()
"Forces undo history to collapse, we invoke when the user is
trying to do an undo command so the undo itself is not collapsed."
(when (memq (current-buffer) md-utterance-changed-buffers)
(md-undo-collapse-end md-collapse-undo-marker)
(setq md-utterance-changed-buffers (delq (current-buffer) md-utterance-changed-buffers))))
(defun md-resume-collapse-after-undo ()
"After the 'undo' part of the utterance has passed, we still want to
collapse anything that comes after."
(when md-in-utterance
(md-check-undo-before-change nil nil)))
(defun md-enable-utterance-undo ()
(setq md-utterance-changed-buffers nil)
(when (featurep 'undo-tree)
(advice-add #'md-force-collapse-undo :before #'undo-tree-undo)
(advice-add #'md-resume-collapse-after-undo :after #'undo-tree-undo)
(advice-add #'md-force-collapse-undo :before #'undo-tree-redo)
(advice-add #'md-resume-collapse-after-undo :after #'undo-tree-redo))
(advice-add #'md-force-collapse-undo :before #'undo)
(advice-add #'md-resume-collapse-after-undo :after #'undo)
(add-hook 'before-change-functions #'md-check-undo-before-change)
(add-hook 'md-start-utterance-hooks #'md-pre-utterance-undo-setup)
(add-hook 'md-end-utterance-hooks #'md-post-utterance-collapse-undo))
(defun md-disable-utterance-undo ()
;;(md-force-collapse-undo)
(when (featurep 'undo-tree)
(advice-remove #'md-force-collapse-undo :before #'undo-tree-undo)
(advice-remove #'md-resume-collapse-after-undo :after #'undo-tree-undo)
(advice-remove #'md-force-collapse-undo :before #'undo-tree-redo)
(advice-remove #'md-resume-collapse-after-undo :after #'undo-tree-redo))
(advice-remove #'md-force-collapse-undo :before #'undo)
(advice-remove #'md-resume-collapse-after-undo :after #'undo)
(remove-hook 'before-change-functions #'md-check-undo-before-change)
(remove-hook 'md-start-utterance-hooks #'md-pre-utterance-undo-setup)
(remove-hook 'md-end-utterance-hooks #'md-post-utterance-collapse-undo))
(md-enable-utterance-undo)
;; (md-disable-utterance-undo)
fuente
buffer-undo-list
como marcador, ¿quizás una entrada del formulario(apply FUN-NAME . ARGS)
? Luego, para deshacer un enunciado, llame repetidamenteundo
hasta encontrar su próximo marcador. Pero sospecho que hay todo tipo de complicaciones aquí. :)Respuestas:
Curiosamente, parece que no hay una función integrada para hacer eso.
El siguiente código funciona insertando un marcador único
buffer-undo-list
en el comienzo de un bloque plegable, y eliminando todos los límites (nil
elementos) al final de un bloque, y luego eliminando el marcador. En caso de que algo salga mal, el marcador tiene la forma(apply identity nil)
de garantizar que no haga nada si permanece en la lista de deshacer.Idealmente, debe usar la
with-undo-collapse
macro, no las funciones subyacentes. Como mencionó que no puede hacer el ajuste, asegúrese de pasar a los marcadores de funciones de bajo nivel que soneq
, no soloequal
.Si el código invocado cambia los búferes, debe asegurarse de que
undo-collapse-end
se llame en el mismo búfer queundo-collapse-begin
. En ese caso, solo las entradas de deshacer en el búfer inicial se contraerán.Aquí hay un ejemplo de uso:
fuente
(apply identity nil)
no hará nada si la llamaprimitive-undo
, no romperá nada si por alguna razón se deja en la lista.(eq (cadr l) nil)
lugar de(null (cadr l))
?Algunos cambios en la maquinaria de deshacer "recientemente" rompieron un truco que
viper-mode
estaba usando para hacer este tipo de colapso (para los curiosos, se usa en el siguiente caso: cuando presionas ESCpara finalizar una inserción / reemplazo / edición, Viper quiere colapsar todo cambiar a un solo paso de deshacer).Para arreglarlo limpiamente, introdujimos una nueva función
undo-amalgamate-change-group
(que corresponde más o menos a suundo-stop-collapsing
) y reutiliza la existenteprepare-change-group
para marcar el comienzo (es decir, corresponde más o menos a suundo-start-collapsing
).Como referencia, aquí está el nuevo código Viper correspondiente:
Esta nueva función aparecerá en Emacs-26, por lo que si desea usarla mientras tanto, puede copiar su definición (requiere
cl-lib
):fuente
undo-amalgamate-change-group
y no parece haber una forma conveniente de usar esto como lawith-undo-collapse
macro definida en esta página, yaatomic-change-group
que no funciona de una manera que permita llamar al grupo conundo-amalgamate-change-group
.atomic-change-group
: lo usa conprepare-change-group
, lo que devuelve el identificador que luego debe pasarundo-amalgamate-change-group
cuando haya terminado.(with-undo-amalgamate ...)
que maneja las cosas del grupo de cambio. De lo contrario, esto es un poco complicado para colapsar algunas operaciones.Aquí hay una
with-undo-collapse
macro que usa la función Emacs-26 change-groups.Esto es
atomic-change-group
con un cambio de una línea, agregandoundo-amalgamate-change-group
.Tiene las ventajas de que:
fuente