Comment effondrer annuler l'histoire?

17

Je travaille sur un mode Emacs qui vous permet de contrôler Emacs avec la reconnaissance vocale. L'un des problèmes que j'ai rencontrés est que la façon dont Emacs gère l'annulation ne correspond pas à la façon dont vous vous attendez à ce que cela fonctionne lors du contrôle par la voix.

Lorsque l'utilisateur prononce plusieurs mots puis s'arrête, cela s'appelle un «énoncé». Un énoncé peut consister en plusieurs commandes à exécuter par Emacs. Il arrive souvent que le module de reconnaissance ne reconnaisse pas correctement une ou plusieurs commandes dans un énoncé. À ce stade, je veux pouvoir dire "annuler" et demander à Emacs d'annuler toutes les actions effectuées par l'énoncé, pas seulement la dernière action de l'énoncé. En d'autres termes, je veux qu'Emacs traite un énoncé comme une seule commande en ce qui concerne l'annulation, même lorsqu'un énoncé se compose de plusieurs commandes. Je voudrais également revenir sur l'endroit où il se trouvait avant l'énoncé, j'ai remarqué que la suppression normale d'Emacs ne fait pas cela.

J'ai configuré Emacs pour obtenir des rappels au début et à la fin de chaque énoncé, afin que je puisse détecter la situation, j'ai juste besoin de comprendre ce que doit faire Emacs. Idéalement, j'appellerais quelque chose comme ça (undo-start-collapsing)et puis (undo-stop-collapsing)et tout ce qui se ferait entre les deux serait magiquement regroupé en un seul enregistrement.

J'ai parcouru la documentation et trouvé undo-boundary, mais c'est l'opposé de ce que je veux - j'ai besoin de réduire toutes les actions dans un énoncé en un seul enregistrement d'annulation, pas de les diviser. Je peux utiliser undo-boundaryentre les énoncés pour m'assurer que les insertions sont considérées comme distinctes (Emacs considère par défaut les actions d'insertion consécutives comme une action jusqu'à une certaine limite), mais c'est tout.

Autres complications:

  • Mon démon de reconnaissance vocale envoie des commandes à Emacs en simulant des pressions de touches X11 et en envoie via, emacsclient -edonc s'il y avait un (undo-collapse &rest ACTIONS)endroit central que je ne peux pas envelopper.
  • J'utilise undo-tree, je ne sais pas si cela rend les choses plus compliquées. Idéalement, une solution fonctionnerait avec le undo-treecomportement d'annulation normal d'Emacs.
  • Que se passe-t-il si l'une des commandes d'un énoncé est "annuler" ou "rétablir"? Je pense que je pourrais changer la logique de rappel pour toujours les envoyer à Emacs en tant qu'énonciations distinctes pour garder les choses plus simples, alors cela devrait être géré comme s'il s'agissait du clavier.
  • Objectif d'extension: un énoncé peut contenir une commande qui commute la fenêtre ou le tampon actuellement actif. Dans ce cas, c'est bien d'avoir à dire "annuler" une fois séparément dans chaque tampon, je n'ai pas besoin que ce soit si sophistiqué. Mais toutes les commandes dans un seul tampon doivent toujours être groupées, donc si je dis "do-x do-y do-z switch-buffer do-a do-b do-c" alors x, y, z devraient être un défaire enregistrement dans le tampon d'origine et a, b, c doivent être un enregistrement dans le tampon commuté.

Y a-t-il un moyen facile de faire ceci? AFAICT il n'y a rien de intégré mais Emacs est vaste et profond ...

Mise à jour: j'ai fini par utiliser la solution de jhc ci-dessous avec un peu de code supplémentaire. Dans le global, before-change-hookje vérifie si le tampon en cours de modification se trouve dans une liste globale de tampons modifiés cet énoncé, sinon il va dans la liste et undo-collapse-beginest appelé. Ensuite, à la fin de l'énoncé, j'itère tous les tampons de la liste et j'appelle undo-collapse-end. Code ci-dessous (md- ajouté avant les noms de fonction à des fins d'espacement de noms):

(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
la source
Pas au courant d'un mécanisme intégré pour cela. Vous pourriez être en mesure d'insérer vos propres entrées dans le en buffer-undo-listtant que marqueur - peut-être une entrée du formulaire (apply FUN-NAME . ARGS)? Ensuite, pour annuler un énoncé, vous appelez à plusieurs reprises undojusqu'à ce que vous trouviez votre prochain marqueur. Mais je soupçonne qu'il y a toutes sortes de complications ici. :)
glucas
Supprimer les frontières semblerait être un meilleur pari.
2015
La manipulation de buffer-undo-list fonctionne-t-elle si j'utilise undo-tree? Je le vois référencé dans la source undo-tree, donc je suppose que oui, mais donner un sens à l'ensemble du mode serait un gros effort.
Joseph Garvin
@JosephGarvin Je suis également intéressé à contrôler Emacs avec la parole. Avez-vous une source disponible?
PythonNut du
@PythonNut: oui :) github.com/jgarvin/mandimus l'emballage est incomplet ... et le code est également partiellement dans mon repo joe-etc: p Mais je l'utilise toute la journée et ça marche.
Joseph Garvin

Réponses:

13

Chose intéressante, il ne semble pas y avoir de fonction intégrée pour le faire.

Le code suivant fonctionne en insérant un marqueur unique buffer-undo-listau début d'un bloc réductible, en supprimant toutes les limites ( niléléments) à la fin d'un bloc, puis en supprimant le marqueur. En cas de problème, le marqueur est de la forme (apply identity nil)pour s'assurer qu'il ne fait rien s'il reste sur la liste d'annulation.

Idéalement, vous devez utiliser la with-undo-collapsemacro, pas les fonctions sous-jacentes. Puisque vous avez mentionné que vous ne pouvez pas effectuer le wrapping, assurez-vous de passer aux marqueurs de fonctions de bas niveau qui le sont eq, pas seulement equal.

Si le code invoqué change de tampon, vous devez vous assurer qu'il undo-collapse-endest appelé dans le même tampon que undo-collapse-begin. Dans ce cas, seules les entrées d'annulation du tampon initial seront réduites.

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

Voici un exemple d'utilisation:

(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
la source
Je comprends pourquoi votre marqueur est une nouvelle liste, mais y a-t-il une raison pour ces éléments spécifiques?
Malabarba
@Malabarba c'est parce qu'une entrée (apply identity nil)ne fera rien si vous l'appelez primitive-undo- elle ne cassera rien si pour une raison quelconque elle est laissée sur la liste.
jch
Mise à jour de ma question pour inclure le code que j'ai ajouté. Merci!
Joseph Garvin
Une raison de faire à la (eq (cadr l) nil)place de (null (cadr l))?
ideasman42
@ ideasman42 modifié selon votre suggestion.
jch
3

Certaines modifications apportées à la machine à annuler "récemment" ont cassé un hack viper-modeutilisé pour effectuer ce type d'effondrement (pour les curieux, il est utilisé dans le cas suivant: lorsque vous appuyez sur ESCpour terminer une insertion / remplacement / édition, Viper veut réduire l'ensemble changer en une seule étape d'annulation).

Pour le corriger proprement, nous avons introduit une nouvelle fonction undo-amalgamate-change-group(qui correspond plus ou moins à la vôtre undo-stop-collapsing) et réutilise l'existant prepare-change-grouppour marquer le début (c'est à dire correspond plus ou moins à la vôtre undo-start-collapsing).

Pour référence, voici le nouveau code Viper correspondant:

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

Cette nouvelle fonction apparaîtra dans Emacs-26, donc si vous voulez l'utiliser entre-temps, vous pouvez copier sa définition (nécessite 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
la source
J'ai regardé undo-amalgamate-change-group, et il ne semble pas y avoir de moyen pratique d'utiliser cela comme la with-undo-collapsemacro définie sur cette page, car atomic-change-groupcela ne fonctionne pas d'une manière qui permet d'appeler le groupe avec undo-amalgamate-change-group.
ideasman42
Bien sûr, vous ne l'utilisez pas avec atomic-change-group: vous l'utilisez avec prepare-change-group, qui renvoie le handle que vous avez ensuite dû passer undo-amalgamate-change-grouplorsque vous avez terminé.
Stefan
Une macro qui traite de cela ne serait-elle pas utile? (with-undo-amalgamate ...)qui gère les éléments du groupe de changement. Sinon, c'est un peu compliqué pour réduire quelques opérations.
ideasman42
Jusqu'à présent, il n'est utilisé que par viper IIRC et Viper ne pourrait pas utiliser une telle macro car les deux appels se produisent dans des commandes distinctes, il n'y a donc pas besoin de pleurer. Mais ce serait trivial d'écrire une telle macro, bien sûr.
Stefan
1
Cette macro pourrait-elle être écrite et incluse dans emacs? Alors que pour un développeur expérimenté, c'est trivial, pour quelqu'un qui veut réduire son historique d'annulation et ne sait pas par où commencer - il est un peu temps de jouer en ligne et de trébucher sur ce fil ... puis de trouver quelle réponse est la meilleure - quand ils ne sont pas assez expérimentés pour pouvoir le dire. J'ai ajouté une réponse ici: emacs.stackexchange.com/a/54412/2418
ideasman42
2

Voici une with-undo-collapsemacro qui utilise la fonction Emacs-26 change-groups.

C'est atomic-change-groupavec un changement d'une ligne, en ajoutant undo-amalgamate-change-group.

Il présente les avantages suivants:

  • Il n'a pas besoin de manipuler directement les données d'annulation.
  • Il garantit que les données d'annulation ne sont pas tronquées.
(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
la source