Wie kann ich die Geschichte rückgängig machen?

17

Ich arbeite an einem Emacs-Modus, mit dem Sie Emacs mit Spracherkennung steuern können. Eines der Probleme, auf das ich gestoßen bin, ist, dass die Art und Weise, wie Emacs mit dem Rückgängigmachen umgeht, nicht mit der erwarteten Funktionsweise bei der Steuerung per Sprache übereinstimmt.

Wenn der Benutzer mehrere Wörter spricht und dann pausiert, spricht man von einer Äußerung. Eine Äußerung kann aus mehreren Befehlen bestehen, die Emacs ausführen soll. Es ist häufig der Fall, dass der Erkenner einen oder mehrere Befehle innerhalb einer Äußerung falsch erkennt. An diesem Punkt möchte ich in der Lage sein, "Rückgängig" zu sagen und Emacs alle Aktionen rückgängig machen zu lassen , die von der Äußerung ausgeführt werden, nicht nur die letzte Aktion innerhalb der Äußerung. Mit anderen Worten, ich möchte, dass Emacs eine Äußerung in Bezug auf das Rückgängigmachen als einen einzigen Befehl behandelt, auch wenn eine Äußerung aus mehreren Befehlen besteht. Ich würde auch gerne darauf hinweisen, genau dorthin zurückzukehren, wo es vor der Äußerung war. Mir ist aufgefallen, dass normale Emacs-Rückgängigmachungen dies nicht tun.

Ich habe Emacs so eingerichtet, dass zu Beginn und am Ende jeder Äußerung Rückrufe erfolgen, damit ich die Situation erkennen kann. Ich muss nur herausfinden, was Emacs tun soll. Im Idealfall würde ich so etwas wie (undo-start-collapsing)und dann nennen (undo-stop-collapsing)und alles, was dazwischen gemacht wird, würde auf magische Weise zu einer einzigen Platte zusammengefasst.

Ich habe die Dokumentation durchsucht und festgestellt undo-boundary, aber es ist das Gegenteil von dem, was ich will - ich muss alle Aktionen innerhalb einer Äußerung in einem Undo-Datensatz zusammenfassen und nicht aufteilen. Ich kann undo-boundaryzwischen Äußerungen verwenden, um sicherzustellen, dass Einfügungen als getrennt betrachtet werden (Emacs betrachtet aufeinanderfolgende Einfügeaktionen standardmäßig bis zu einer gewissen Grenze als eine Aktion), aber das war's.

Andere Komplikationen:

  • Meine Spracherkennung Daemon sendet einige Befehle Emacs von X11 Drücken von Tasten simuliert und sendet über einige emacsclient -eso, wenn es sagen würde eine (undo-collapse &rest ACTIONS)gibt es keinen zentralen Ort , wo ich wickeln kann.
  • Ich bin mir undo-treenicht sicher, ob das die Dinge komplizierter macht. Idealerweise würde eine Lösung mit undo-treedem normalen Rückgängig-Verhalten von Emacs funktionieren .
  • Was ist, wenn einer der Befehle in einer Äußerung "Rückgängig" oder "Wiederherstellen" ist? Ich denke, ich könnte die Rückruflogik ändern, um diese immer als eindeutige Äußerungen an Emacs zu senden, um die Dinge einfacher zu halten. Dann sollte es genauso gehandhabt werden, als ob ich die Tastatur benutzen würde.
  • Ziel strecken: Eine Äußerung kann einen Befehl enthalten, der das aktuell aktive Fenster oder den Puffer wechselt. In diesem Fall ist es in Ordnung, in jedem Puffer einmal "rückgängig machen" zu müssen, ich brauche es nicht, um so schick zu sein. Aber alle Befehle in einem einzelnen Puffer sollten immer noch gruppiert sein. Wenn ich also "do-x do-y do-z Schaltpuffer do-a do-b do-c" sage, sollte x, y, z eins sein undo Datensatz im ursprünglichen Puffer und a, b, c sollten ein Datensatz im Umschaltpuffer sein.

Gibt es eine einfache Möglichkeit, dies zu tun? AFAICT, es ist nichts eingebaut, aber Emacs ist riesig und tief ...

Update: Am Ende habe ich die unten stehende jhc-Lösung mit ein wenig zusätzlichem Code verwendet. Bei der globalen before-change-hookPrüfung überprüfe ich, ob der zu ändernde Puffer in einer globalen Liste von Puffern enthalten ist, die diese Äußerung modifiziert haben, wenn sie nicht in die Liste eingeht und undo-collapse-beginaufgerufen wird. Am Ende der Äußerung durchlaufe ich dann alle Puffer in der Liste und rufe auf undo-collapse-end. Code unten (md- vor Funktionsnamen für Namespace-Zwecke hinzugefügt):

(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
quelle
Keine Kenntnis von einem eingebauten Mechanismus dafür. Möglicherweise können Sie Ihre eigenen Einträge buffer-undo-listals Markierung in das einfügen - vielleicht einen Eintrag des Formulars (apply FUN-NAME . ARGS)? Um eine Äußerung rückgängig zu machen, rufen Sie wiederholt auf, undobis Sie Ihren nächsten Marker gefunden haben. Aber ich vermute, dass es hier alle möglichen Komplikationen gibt. :)
glucas
Das Aufheben von Grenzen wäre eine bessere Wahl.
19.
Funktioniert die Bearbeitung der Buffer-Undo-Liste, wenn ich Undo-Tree verwende? Ich sehe, dass es in der Undo-Tree-Quelle referenziert ist, also schätze ich ja, aber es wäre ein großes Unterfangen, den gesamten Modus zu verstehen.
Joseph Garvin
@JosephGarvin Ich bin daran interessiert, Emacs auch mit Sprache zu steuern. Haben Sie eine Quelle zur Verfügung?
PythonNut
@PythonNut: ja :) github.com/jgarvin/mandimus die verpackung ist unvollständig ... und der code ist auch teilweise in meinem joe-etc repo: p aber ich benutze es den ganzen tag und es funktioniert.
Joseph Garvin

Antworten:

13

Interessanterweise scheint es dafür keine eingebaute Funktion zu geben.

Im folgenden Code wird eine eindeutige Markierung am buffer-undo-listAnfang eines reduzierbaren Blocks eingefügt und alle Begrenzungen ( nilElemente) am Ende eines Blocks entfernt. Anschließend wird die Markierung entfernt. Falls etwas schief geht, hat der Marker die Form, (apply identity nil)um sicherzustellen, dass er nichts tut, wenn er auf der Rückgängig-Liste bleibt.

Im Idealfall sollten Sie das with-undo-collapseMakro verwenden, nicht die zugrunde liegenden Funktionen. Da Sie erwähnt haben, dass Sie den Zeilenumbruch nicht ausführen können, stellen Sie sicher, dass Sie zu den Funktionsmarkierungen auf niedriger Ebene übergehen, bei denen es eqsich nicht nur um Markierungen handelt equal.

Wenn der aufgerufene Code Puffer wechselt, müssen Sie sicherstellen, dass dieser undo-collapse-endim gleichen Puffer wie aufgerufen wird undo-collapse-begin. In diesem Fall werden nur die Rückgängig-Einträge im Anfangspuffer reduziert.

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

Hier ist ein Anwendungsbeispiel:

(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
quelle
Ich verstehe, warum Ihr Marker eine neue Liste ist, aber gibt es einen Grund für diese spezifischen Elemente?
Malabarba,
@Malabarba Das liegt daran, dass ein Eintrag (apply identity nil)nichts bewirkt, wenn Sie ihn aufrufen. primitive-undoEr bricht nichts, wenn er aus irgendeinem Grund in der Liste verbleibt .
20.
Meine Frage wurde aktualisiert und enthält nun den Code, den ich hinzugefügt habe. Vielen Dank!
Joseph Garvin
Irgendein Grund, (eq (cadr l) nil)statt zu tun (null (cadr l))?
ideasman42
@ ideasman42 geändert nach Ihrem Vorschlag.
5.
3

Einige Änderungen an der Undo-Maschinerie haben "vor kurzem" dazu geführt, dass ein Hack viper-modediese Art von Zusammenbruch durchführte (für die Neugierigen wird dies in folgendem Fall verwendet: Wenn Sie drücken ESC, um eine Einfügung / Ersetzung / Ausgabe zu beenden, möchte Viper das Ganze zusammenbrechen in einen einzigen Undo-Schritt verwandeln).

Um es sauber zu machen, haben wir eine neue Funktion eingeführt undo-amalgamate-change-group(die mehr oder weniger Ihrer entspricht undo-stop-collapsing) und die vorhandene wiederverwendet prepare-change-group, um den Anfang zu markieren (dh sie entspricht mehr oder weniger Ihrer undo-start-collapsing).

Als Referenz ist hier der entsprechende neue Viper-Code:

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

Diese neue Funktion wird in Emacs-26 angezeigt. Wenn Sie sie in der Zwischenzeit verwenden möchten, können Sie ihre Definition kopieren (erfordert 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
quelle
Ich habe nachgesehen undo-amalgamate-change-group, und es scheint keine bequeme Möglichkeit zu geben, dies wie das with-undo-collapseauf dieser Seite definierte Makro zu verwenden , da atomic-change-groupes nicht so funktioniert, dass die Gruppe mit aufgerufen werden kann undo-amalgamate-change-group.
ideasman42
Natürlich verwenden Sie es nicht mit atomic-change-group: Sie verwenden es mit prepare-change-group, wodurch das Handle zurückgegeben wird, an das Sie dann übergeben müssen, undo-amalgamate-change-groupwenn Sie fertig sind.
Stefan
Wäre ein Makro, das sich damit befasst, nicht nützlich? (with-undo-amalgamate ...)welches die Änderungsgruppen-Sachen handhabt. Ansonsten ist es ein bisschen mühsam, ein paar Operationen zusammenzubrechen.
ideasman42
Bisher wird es nur von Viper IIRC verwendet, und Viper wäre nicht in der Lage, ein solches Makro zu verwenden, da die beiden Aufrufe in getrennten Befehlen erfolgen. Aber es wäre natürlich trivial, ein solches Makro zu schreiben.
Stefan
1
Könnte dieses Makro geschrieben und in Emacs enthalten sein? Während es für einen erfahrenen Entwickler trivial ist, für jemanden, der seine Rückgängigmachungshistorie zusammenbrechen möchte und nicht weiß, wo er anfangen soll, ist es eine Weile, online herumzuspielen und über diesen Thread zu stolpern. Dann muss er herausfinden, welche Antwort die beste ist. wenn sie nicht erfahren genug sind, um es zu sagen. Ich habe hier eine Antwort hinzugefügt: emacs.stackexchange.com/a/54412/2418
ideasman42
2

Dies ist ein with-undo-collapseMakro, das die Emacs-26-Funktion zum Ändern von Gruppen verwendet.

Dies wird atomic-change-groupmit einer Zeile geändert und hinzugefügt undo-amalgamate-change-group.

Es hat die Vorteile, dass:

  • Die Rückgängig-Daten müssen nicht direkt bearbeitet werden.
  • Es stellt sicher, dass nicht rückgängig gemachte Daten abgeschnitten werden.
(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
quelle