Como recolher desfazer a história?

17

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-boundaryentre 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 -eisso, 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 com undo-treeo 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)
Joseph Garvin
fonte
Não tem conhecimento de um mecanismo interno para isso. Você pode inserir suas próprias entradas no buffer-undo-listmarcador - talvez uma entrada do formulário (apply FUN-NAME . ARGS)? Em seguida, para desfazer uma expressão que você chama repetidamente undoaté encontrar o seu próximo marcador. Mas suspeito que haja todo tipo de complicações aqui. :)
glucas
Remover limites pareceria uma aposta melhor.
JCH
A manipulação de lista de desfazer buffer funciona se eu estiver usando desfazer árvore? Eu o vejo referenciado na fonte desfazer a árvore, então acho que sim, mas entender todo o modo seria um grande esforço.
Joseph Garvin
@ JosephphGarvin Também estou interessado em controlar o Emacs com a fala. Você tem alguma fonte disponível?
precisa saber é o seguinte
@ PythonNut: yes :) github.com/jgarvin/mandimus a embalagem está incompleta ... e o código também está parcialmente no meu repositório joe-etc: p Mas eu uso o dia todo e funciona.
Joseph Garvin

Respostas:

13

Curiosamente, parece não haver função interna para fazer isso.

O código a seguir funciona inserindo um marcador exclusivo buffer-undo-listno início de um bloco recolhível e removendo todos os limites ( nilelementos) 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-collapsemacro, 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 que eqnão são apenas equal.

Se o código chamado alternar buffers, você deve garantir que isso undo-collapse-endseja chamado no mesmo buffer que undo-collapse-begin. Nesse caso, apenas as entradas desfazer no buffer inicial serão recolhidas.

(defun 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."
  (push marker buffer-undo-list))

(defun undo-collapse-end (marker)
  "Collapse undo history until a matching marker."
  (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 "undo-collapse-end with no matching marker"))
           ((null (cadr l))
            (setf (cdr l) (cddr l)))
           (t (setq l (cdr l)))))
       ;; remove the marker
       (setf (cdr l) (cddr l))))))

 (defmacro with-undo-collapse (&rest body)
  "Execute body, then collapse any resulting undo boundaries."
  (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
              (undo-collapse-begin ',marker)
              ,@body)
         (with-current-buffer ,buffer-var
           (undo-collapse-end ',marker))))))

Aqui está um exemplo de uso:

(defun test-no-collapse ()
  (interactive)
  (insert "toto")
  (undo-boundary)
  (insert "titi"))

(defun test-collapse ()
  (interactive)
  (with-undo-collapse
    (insert "toto")
    (undo-boundary)
    (insert "titi")))
jch
fonte
Entendo por que seu marcador é uma lista nova, mas há uma razão para esses elementos específicos?
Malabarba
@ Malabarba, porque uma entrada (apply identity nil)não fará nada se você a chamar primitive-undo- não quebrará nada se, por algum motivo, for deixada na lista.
JCH
Atualizei minha pergunta para incluir o código que adicionei. Obrigado!
Joseph Garvin
Alguma razão para fazer em (eq (cadr l) nil)vez de (null (cadr l))?
usar o seguinte código
@ ideasman42 modificado de acordo com a sua sugestão.
JCH
3

Algumas mudanças no mecanismo de desfazer "recentemente" quebraram algum tipo de invasão que viper-modeestava 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 à sua undo-stop-collapsing) e reutilizamos a existente prepare-change-grouppara marcar o início (ou seja, corresponde mais ou menos à sua undo-start-collapsing).

Para referência, aqui está o novo código Viper correspondente:

(viper-deflocalvar viper--undo-change-group-handle nil)
(put 'viper--undo-change-group-handle 'permanent-local t)

(defun viper-adjust-undo ()
  (when viper--undo-change-group-handle
    (undo-amalgamate-change-group
     (prog1 viper--undo-change-group-handle
       (setq viper--undo-change-group-handle nil)))))

(defun viper-set-complex-command-for-undo ()
  (and (listp buffer-undo-list)
       (not viper--undo-change-group-handle)
       (setq viper--undo-change-group-handle
             (prepare-change-group))))

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

(defun undo-amalgamate-change-group (handle)
  "Amalgamate changes in change-group since HANDLE.
Remove all undo boundaries between the state of HANDLE and now.
HANDLE is as returned by `prepare-change-group'."
  (dolist (elt handle)
    (with-current-buffer (car elt)
      (setq elt (cdr elt))
      (when (consp buffer-undo-list)
        (let ((old-car (car-safe elt))
              (old-cdr (cdr-safe elt)))
          (unwind-protect
              (progn
                ;; Temporarily truncate the undo log at ELT.
                (when (consp elt)
                  (setcar elt t) (setcdr elt nil))
                (when
                    (or (null elt)        ;The undo-log was empty.
                        ;; `elt' is still in the log: normal case.
                        (eq elt (last buffer-undo-list))
                        ;; `elt' is not in the log any more, but that's because
                        ;; the log is "all new", so we should remove all
                        ;; boundaries from it.
                        (not (eq (last buffer-undo-list) (last old-cdr))))
                  (cl-callf (lambda (x) (delq nil x))
                      (if (car buffer-undo-list)
                          buffer-undo-list
                        ;; Preserve the undo-boundaries at either ends of the
                        ;; change-groups.
                        (cdr buffer-undo-list)))))
            ;; Reset the modified cons cell ELT to its original content.
            (when (consp elt)
              (setcar elt old-car)
              (setcdr elt old-cdr))))))))
Stefan
fonte
Examinei undo-amalgamate-change-groupe não parece haver uma maneira conveniente de usar isso como a with-undo-collapsemacro definida nesta página, pois atomic-change-groupnão funciona de uma maneira que permita chamar o grupo undo-amalgamate-change-group.
usar o seguinte código
Obviamente, você não o usa com atomic-change-group: você usa com prepare-change-group, que retorna o identificador que você precisava passar undo-amalgamate-change-groupquando terminar.
Stefan
Uma macro que lida com isso não seria útil? (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.
usar o seguinte código
Até o momento, ele é usado apenas pelo viper IIRC e o Viper não seria capaz de usar essa macro porque as duas chamadas acontecem em comandos separados, portanto, não há necessidade disso. Mas seria trivial escrever essa macro, é claro.
Stefan
1
Essa macro poderia ser escrita e incluída no emacs? Embora para um desenvolvedor experiente seja trivial, para alguém que deseja recolher o histórico de desfazer e não sabe por onde começar - é necessário algum tempo brincando online e tropeçando nesse tópico ... depois, é preciso descobrir qual é a melhor resposta - quando eles não são experientes o suficiente para poderem dizer. Adicionei
ideasman42
2

Esta é uma with-undo-collapsemacro que usa o recurso de grupos de alterações Emacs-26.

Isso ocorre atomic-change-groupcom uma alteração de uma linha, adicionando undo-amalgamate-change-group.

Tem as vantagens que:

  • Ele não precisa manipular os dados de desfazer diretamente.
  • Isso garante que os dados de desfazer não sejam truncados.
(defmacro with-undo-collapse (&rest body)
  "Like `progn' but perform BODY with undo collapsed."
  (declare (indent 0) (debug t))
  (let ((handle (make-symbol "--change-group-handle--"))
        (success (make-symbol "--change-group-success--")))
    `(let ((,handle (prepare-change-group))
            ;; Don't truncate any undo data in the middle of this.
            (undo-outer-limit nil)
            (undo-limit most-positive-fixnum)
            (undo-strong-limit most-positive-fixnum)
            (,success nil))
       (unwind-protect
         (progn
           (activate-change-group ,handle)
           (prog1 ,(macroexp-progn body)
             (setq ,success t)))
         (if ,success
           (progn
             (accept-change-group ,handle)
             (undo-amalgamate-change-group ,handle))
           (cancel-change-group ,handle))))))
ideasman42
fonte