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)
fonte
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. :)Respostas:
Curiosamente, parece não haver função interna para fazer isso.
O código a seguir funciona inserindo um marcador exclusivo
buffer-undo-list
no início de um bloco recolhível e removendo todos os limites (nil
elementos) no final de um bloco e removendo o marcador. Caso algo dê errado, o marcador terá a forma(apply identity nil)
de garantir que não fará nada se permanecer na lista de desfazer.Idealmente, você deve usar a
with-undo-collapse
macro, não as funções subjacentes. Como você mencionou que não pode fazer a quebra, certifique-se de passar para os marcadores de funções de baixo nível queeq
não são apenasequal
.Se o código chamado alternar buffers, você deve garantir que isso
undo-collapse-end
seja chamado no mesmo buffer queundo-collapse-begin
. Nesse caso, apenas as entradas desfazer no buffer inicial serão recolhidas.Aqui está um exemplo de uso:
fonte
(apply identity nil)
não fará nada se você a chamarprimitive-undo
- não quebrará nada se, por algum motivo, for deixada na lista.(eq (cadr l) nil)
vez de(null (cadr l))
?Algumas mudanças no mecanismo de desfazer "recentemente" quebraram algum tipo de invasão que
viper-mode
estava sendo usada para fazer esse tipo de colapso (para os curiosos, é usado no seguinte caso: quando você pressiona ESCpara concluir uma inserção / substituição / edição, o Viper deseja recolher o conjunto mudar para uma única etapa de desfazer).Para corrigi-lo corretamente, introduzimos uma nova função
undo-amalgamate-change-group
(que corresponde mais ou menos à suaundo-stop-collapsing
) e reutilizamos a existenteprepare-change-group
para marcar o início (ou seja, corresponde mais ou menos à suaundo-start-collapsing
).Para referência, aqui está o novo código Viper correspondente:
Essa nova função aparecerá no Emacs-26; portanto, se você quiser usá-la nesse meio tempo, poderá copiar sua definição (requer
cl-lib
):fonte
undo-amalgamate-change-group
e não parece haver uma maneira conveniente de usar isso como awith-undo-collapse
macro definida nesta página, poisatomic-change-group
não funciona de uma maneira que permita chamar o grupoundo-amalgamate-change-group
.atomic-change-group
: você usa comprepare-change-group
, que retorna o identificador que você precisava passarundo-amalgamate-change-group
quando terminar.(with-undo-amalgamate ...)
que lida com as coisas do grupo de alterações. Caso contrário, isso é um pouco trabalhoso para recolher algumas operações.Esta é uma
with-undo-collapse
macro que usa o recurso de grupos de alterações Emacs-26.Isso ocorre
atomic-change-group
com uma alteração de uma linha, adicionandoundo-amalgamate-change-group
.Tem as vantagens que:
fonte