Estou trabalhando no modo Emacs que permite controlar o Emacs com reconhecimento de fala. Um dos problemas que encontrei é que a maneira como o Emacs lida com desfazer não corresponde à forma como você esperaria que ele funcionasse ao controlar por voz.
Quando o usuário fala várias palavras e depois faz uma pausa, isso é chamado de 'enunciado'. Um enunciado pode consistir em vários comandos para o Emacs executar. Geralmente, o reconhecedor reconhece um ou mais comandos dentro de uma expressão incorretamente. Nesse ponto, eu quero poder dizer "desfazer" e fazer com que o Emacs desfaça todas as ações executadas pelo enunciado, não apenas a última ação do enunciado. Em outras palavras, quero que o Emacs trate um enunciado como um único comando no que diz respeito a desfazer, mesmo quando um enunciado consiste em vários comandos. Eu também gostaria de apontar para voltar exatamente ao que estava antes do pronunciamento, notei que o desfazer normal do Emacs não faz isso.
Eu configurei o Emacs para receber retornos de chamada no início e no final de cada enunciado, para que eu possa detectar a situação, só preciso descobrir o que o Emacs deve fazer. Idealmente, eu chamaria algo assim (undo-start-collapsing)
e então, (undo-stop-collapsing)
qualquer coisa feita entre eles seria magicamente desmoronada em um registro.
Pesquisei a documentação e encontrei undo-boundary
, mas é o oposto do que quero - preciso recolher todas as ações dentro de um enunciado em um único registro de desfazer, não separá-las. Posso usar undo-boundary
entre expressões para garantir que as inserções sejam consideradas separadas (o Emacs, por padrão, considera as ações consecutivas de inserção como uma ação até certo limite), mas é isso.
Outras complicações:
- Meu daemon de reconhecimento de fala envia alguns comandos para o Emacs, simulando pressionamentos de tecla X11 e envia alguns por
emacsclient -e
isso, se houver um que(undo-collapse &rest ACTIONS)
não exista um lugar central que eu possa usar. - Eu uso
undo-tree
, não tenho certeza se isso torna as coisas mais complicadas. Idealmente, uma solução funcionaria comundo-tree
o comportamento normal de desfazer do Emacs. - E se um dos comandos em uma expressão for "desfazer" ou "refazer"? Estou achando que poderia mudar a lógica de retorno de chamada para sempre enviá-las ao Emacs como enunciados distintos para simplificar as coisas, então ele deve ser tratado da mesma maneira que faria se eu estivesse usando o teclado.
- Objetivo de extensão: Uma declaração pode conter um comando que alterna a janela ou o buffer atualmente ativo. Nesse caso, é bom dizer "desfazer" uma vez separadamente em cada buffer, não preciso que seja tão chique. Mas todos os comandos em um único buffer ainda devem ser agrupados, portanto, se eu disser "do-x-do-y-z-switch-buffer-da-b-do-c", então x, y, z deve ser um desfazer registro no buffer original e a, b, c deve ser um registro no comutado para buffer.
Existe uma maneira fácil de fazer isso? AFAICT não há nada embutido, mas o Emacs é vasto e profundo ...
Atualização: Acabei usando a solução da jhc abaixo com um pouco de código extra. No global before-change-hook
, verifico se o buffer que está sendo alterado está em uma lista global de buffers que modificaram esse enunciado; caso contrário, ele entra na lista e undo-collapse-begin
é chamado. Então, no final do enunciado, repito todos os buffers da lista e ligo undo-collapse-end
. Código abaixo (md - adicionado antes dos nomes das funções para fins de namespacing):
(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
marcador - talvez uma entrada do formulário(apply FUN-NAME . ARGS)
? Em seguida, para desfazer uma expressão que você chama repetidamenteundo
até encontrar o seu próximo marcador. Mas suspeito que haja todo tipo de complicações aqui. :)