Jak zwinąć cofnąć historię?

17

Pracuję nad trybem Emacsa, który pozwala kontrolować Emacsa za pomocą rozpoznawania mowy. Jednym z problemów, na jakie natknąłem się, jest to, że sposób, w jaki Emacs obsługuje cofanie, nie pasuje do tego, jak można oczekiwać od sterowania głosowego.

Gdy użytkownik wypowiada kilka słów, a następnie zatrzymuje się, nazywa się to „wypowiedzią”. Wypowiedź może składać się z wielu poleceń Emacsa do wykonania. Często zdarza się, że program rozpoznający nieprawidłowo rozpoznaje jedno lub więcej poleceń w wypowiedzi. W tym momencie chcę móc powiedzieć „cofnąć” i pozwolić Emacsowi cofnąć wszystkie działania wykonane przez wypowiedź, a nie tylko ostatnie działanie w wypowiedzi. Innymi słowy, chcę, aby Emacs traktował wypowiedź jako pojedyncze polecenie w zakresie cofania, nawet jeśli wypowiedź składa się z wielu poleceń. Chciałbym też wrócić do tego, co było przed wypowiedzią, zauważyłem, że normalne cofanie Emacsa tego nie robi.

Skonfigurowałem Emacsa, aby na początku i na końcu każdej wypowiedzi pojawiały się oddzwaniania, więc mogę wykryć sytuację, muszę tylko dowiedzieć się, co zrobić Emacs. Idealnie nazwałbym coś takiego, (undo-start-collapsing)a wtedy (undo-stop-collapsing)wszystko zrobione pomiędzy nimi byłoby magicznie zwinięte w jeden rekord.

Przeszukałem trochę dokumentacji i znalazłem undo-boundary, ale jest to odwrotność tego, czego chcę - muszę zwinąć wszystkie działania w wypowiedzi w jeden rekord cofania, a nie dzielić je. Mogę używać undo-boundarymiędzy wypowiedziami, aby upewnić się, że wstawienia są uważane za osobne (Emacs domyślnie uważa, że ​​kolejne operacje wstawiania są jedną operacją do pewnego limitu), ale to wszystko.

Inne komplikacje:

  • Mój demon rozpoznawania mowy wysyła niektóre polecenia do Emacsa, symulując naciśnięcia klawiszy X11, i przesyła je przez, emacsclient -ewięc jeśli powiedzą, że (undo-collapse &rest ACTIONS)nie ma centralnego miejsca, w którym mógłbym zawinąć.
  • Używam undo-tree, nie jestem pewien, czy to komplikuje sprawę. Idealnie byłoby, gdyby rozwiązanie działało undo-treei normalne zachowanie cofania Emacsa.
  • Co jeśli jednym z poleceń w wypowiedzi jest „cofnij” lub „powtórz”? Myślę, że mógłbym zmienić logikę wywołania zwrotnego, aby zawsze wysyłać je do Emacsa jako odrębne wypowiedzi, aby uprościć sprawę, to powinno być obsługiwane tak samo, jak w przypadku korzystania z klawiatury.
  • Cel rozciągnięcia: Wypowiedź może zawierać polecenie, które przełącza aktualnie aktywne okno lub bufor. W takim przypadku dobrze jest powiedzieć „cofnij” raz osobno w każdym buforze, nie muszę być tak fantazyjny. Ale wszystkie polecenia w jednym buforze powinny być nadal pogrupowane, więc jeśli powiem „do-x do-y do-z przełącznik-bufor do-a do-b do-c”, to x, y, z powinno być cofnięte rekord w oryginalnym buforze, a a, b, c powinny być jednym rekordem w przełączonym buforze.

Czy jest na to łatwy sposób? AFAICT nie ma nic wbudowanego, ale Emacs jest ogromny i głęboki ...

Aktualizacja: Skończyłem używać rozwiązania jhc poniżej z małym dodatkowym kodem. W globalnym before-change-hooksprawdzam, czy zmieniany bufor znajduje się na globalnej liście buforów zmodyfikowanych w tej wypowiedzi, jeśli nie, to trafia na listę i undo-collapse-beginjest wywoływany. Na koniec wypowiedzi iteruję wszystkie bufory na liście i wywołuję undo-collapse-end. Kod poniżej (md - dodany przed nazwami funkcji do celów przestrzeni nazw):

(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
źródło
Nie jestem świadomy wbudowanego mechanizmu do tego. Być może będziesz mógł wstawić własne wpisy do buffer-undo-listznacznika - być może wpis formularza (apply FUN-NAME . ARGS)? Następnie, aby cofnąć wypowiedź, którą wielokrotnie wołasz, undoaż znajdziesz następny znacznik. Ale podejrzewam, że są tutaj różnego rodzaju komplikacje. :)
glucas
Usunięcie granic wydaje się lepszym rozwiązaniem.
jch
Czy manipulowanie buforową listą cofnięć działa, jeśli używam cofania drzewa? Widzę to w źródle cofania drzewa, więc zgaduję, że tak, ale zrozumienie całego trybu byłoby dużym przedsięwzięciem.
Joseph Garvin
@JosephGarvin Interesuje mnie również sterowanie Emacsem za pomocą mowy. Czy masz jakieś dostępne źródło?
PythonNut,
@PythonNut: tak :) github.com/jgarvin/mandimus opakowanie jest niekompletne ... a kod znajduje się również częściowo w moim repozytorium Joe -etc: p Ale używam go cały dzień i działa.
Joseph Garvin

Odpowiedzi:

13

Co ciekawe, wydaje się, że nie ma wbudowanej do tego funkcji.

Poniższy kod działa poprzez wstawienie unikalnego znacznika na buffer-undo-listpoczątku składanego bloku i usunięcie wszystkich granic ( nilelementów) na końcu bloku, a następnie usunięcie znacznika. W przypadku, gdy coś pójdzie nie tak, znacznik ma formę, (apply identity nil)aby upewnić się, że nic nie zrobi, jeśli pozostanie na liście cofnięć.

Najlepiej jest używać with-undo-collapsemakra, a nie podstawowych funkcji. Ponieważ wspomniano, że nie można wykonać owijania, należy przejść do znaczników funkcji niskiego poziomu, które są eqnie tylko equal.

Jeśli wywoływany kod przełącza bufory, musisz upewnić się, że undo-collapse-endjest wywoływany w tym samym buforze co undo-collapse-begin. W takim przypadku tylko wpisy cofania w buforze początkowym zostaną zwinięte.

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

Oto przykład użycia:

(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
źródło
Rozumiem, dlaczego twój marker jest świeżą listą, ale czy istnieje powód dla tych konkretnych elementów?
Malabarba
@Malabarba to dlatego, że wpis (apply identity nil)nic nie zrobi, jeśli do niego zadzwonisz primitive-undo- nic nie zepsuje, jeśli z jakiegoś powodu pozostanie na liście.
jch
Zaktualizowałem moje pytanie, dodając kod, który dodałem. Dzięki!
Joseph Garvin,
Jakikolwiek powód, aby zrobić (eq (cadr l) nil)zamiast (null (cadr l))?
ideasman42
@ ideasman42 zmodyfikowany zgodnie z twoją sugestią.
jch
3

Niektóre zmiany w maszynie cofania „niedawno” złamały jakiś hack viper-modeużywany do tego rodzaju zwijania (dla ciekawskich jest on używany w następującym przypadku: kiedy naciśniesz, ESCaby zakończyć wstawianie / wymianę / edycję, Viper chce zwinąć całość zmienić w jeden krok cofania).

Aby to naprawić, wprowadziliśmy nową funkcję undo-amalgamate-change-group(która odpowiada mniej więcej twojemu undo-stop-collapsing) i ponownie wykorzystuje istniejącą prepare-change-groupdo oznaczenia początku (tj. Odpowiada mniej więcej twojej undo-start-collapsing).

Dla odniesienia, oto odpowiedni nowy kod Vipera:

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

Ta nowa funkcja pojawi się w Emacs-26, więc jeśli chcesz jej używać w międzyczasie, możesz skopiować jej definicję (wymaga 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
źródło
Przyjrzałem się undo-amalgamate-change-groupi wydaje się , że nie ma wygodnego sposobu korzystania z tego, takiego jak with-undo-collapsemakro zdefiniowane na tej stronie, ponieważ atomic-change-groupnie działa w sposób umożliwiający wywołanie grupy undo-amalgamate-change-group.
ideasman42,
Oczywiście nie używasz go atomic-change-group: używasz go prepare-change-group, co zwraca uchwyt, który następnie musisz przekazać, undo-amalgamate-change-groupgdy skończysz.
Stefan
Czy makro, które się tym zajmuje, nie byłoby przydatne? (with-undo-amalgamate ...)który obsługuje zmiany grupy rzeczy. W przeciwnym razie jest to trochę kłopotliwe z powodu zwinięcia kilku operacji.
ideasman42
Do tej pory używa go tylko viper IIRC, a Viper nie byłby w stanie użyć takiego makra, ponieważ te dwa wywołania odbywają się w osobnych poleceniach, więc nie trzeba go płakać. Oczywiście napisanie takiego makra byłoby trywialne.
Stefan
1
Czy to makro można zapisać i włączyć do emacsa? Podczas gdy dla doświadczonego programisty jest to banalne, dla kogoś, kto chce zwinąć swoją historię cofania i nie wie od czego zacząć - jest trochę czasu na zabawę w Internecie i natknięcie się na ten wątek ... a następnie znalezienie odpowiedzi, która jest najlepsza - kiedy nie są wystarczająco doświadczeni, by móc powiedzieć. Dodałem odpowiedź tutaj: emacs.stackexchange.com/a/54412/2418
ideasman42
2

Oto with-undo-collapsemakro korzystające z funkcji grup zmian Emacs-26.

Jest to zmiana atomic-change-groupo jedną linię, dodawanie undo-amalgamate-change-group.

Ma zalety, które:

  • Nie trzeba bezpośrednio manipulować danymi cofania.
  • Zapewnia to, że cofanie danych nie jest obcinane.
(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
źródło