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)
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í. :)