Come chiudere la cronologia degli annullamenti?


17

Sto lavorando a una modalità Emacs che ti consente di controllare Emacs con il riconoscimento vocale. Uno dei problemi che ho riscontrato è che il modo in cui Emacs gestisce l'annullamento non corrisponde a come ci si aspetterebbe che funzioni quando si controlla a voce.

Quando l'utente parla più parole e poi fa una pausa, si parla di "espressione". Un'espressione può consistere in più comandi per l'esecuzione di Emacs. Accade spesso che il riconoscitore riconosca erroneamente uno o più comandi all'interno di un'espressione. A quel punto voglio poter dire "annulla" e fare in modo che Emacs annulli tutte le azioni compiute dall'espressione, non solo l'ultima azione all'interno dell'espressione. In altre parole, voglio che Emacs tratti una frase come un singolo comando per quanto riguarda l'annullamento, anche quando una frase è composta da più comandi. Vorrei anche puntare a tornare esattamente dove era prima dell'espressione, ho notato che il normale annullamento di Emacs non lo fa.

Ho installato Emacs per ottenere i callback all'inizio e alla fine di ogni espressione, così posso rilevare la situazione, ho solo bisogno di capire cosa fare Emacs. Idealmente, chiamerei qualcosa del genere (undo-start-collapsing)e poi (undo-stop-collapsing)e qualsiasi cosa fatta tra di loro sarebbe magicamente crollata in un record.

Ho fatto un po 'di esplorazione della documentazione e ho scoperto undo-boundary, ma è l'opposto di quello che voglio: ho bisogno di comprimere tutte le azioni all'interno di una frase in un record di annullamento, non dividerle. Posso usare undo-boundarytra le espressioni per assicurarmi che gli inserimenti siano considerati separati (per impostazione predefinita Emacs considera le azioni consecutive di inserimento come un'azione fino a un certo limite), ma è tutto.

Altre complicazioni:

  • Il mio demone di riconoscimento vocale invia alcuni comandi a Emacs simulando i tasti X11 e ne invia alcuni tramite emacsclient -etale, se si dicesse che (undo-collapse &rest ACTIONS)non c'è un posto centrale che posso avvolgere.
  • Uso undo-tree, non sono sicuro che ciò renda le cose più complicate. Idealmente, una soluzione dovrebbe funzionare con undo-treeil normale comportamento di annullamento di Emacs.
  • Cosa succede se uno dei comandi all'interno di un enunciato è "annulla" o "ripristina"? Sto pensando che potrei cambiare la logica di callback per inviarli sempre ad Emacs come espressioni distinte per mantenere le cose più semplici, quindi dovrebbe essere gestito proprio come farebbe se usassi la tastiera.
  • Allunga obiettivo: un enunciato può contenere un comando che commuta la finestra o il buffer attualmente attivi. In questo caso va bene dire "annulla" una volta separatamente in ogni buffer, non ho bisogno che sia così elaborato. Ma tutti i comandi in un singolo buffer dovrebbero comunque essere raggruppati, quindi se dico "do-x do-y do-z switch-buffer do-a do-b do-c", allora x, y, z dovrebbero essere uno annulla record nel buffer originale e a, b, c dovrebbe essere un record nel buffer commutato.

C'è un modo semplice per farlo? AFAICT non c'è nulla di integrato ma Emacs è vasto e profondo ...

Aggiornamento: ho finito per usare la soluzione di jhc di seguito con un piccolo codice aggiuntivo. Nel globale before-change-hookcontrollo se il buffer che si sta modificando è in un elenco globale di buffer modificato questa espressione, se non va nell'elenco e undo-collapse-beginviene chiamato. Quindi alla fine dell'iterazione eseguo l'iterazione di tutti i buffer nell'elenco e chiamo undo-collapse-end. Codice seguente (md- aggiunto prima dei nomi delle funzioni a fini di spaziatura dei nomi):

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

Non sono a conoscenza di un meccanismo integrato per questo. Potresti essere in grado di inserire le tue voci buffer-undo-listcome indicatore - forse una voce del modulo (apply FUN-NAME . ARGS)? Quindi per annullare un enunciato, chiedi ripetutamente undofino a trovare il tuo prossimo marker. Ma sospetto che ci siano tutti i tipi di complicazioni qui. :)
glucas,

Rimuovere i confini sembrerebbe una scommessa migliore.
jch

La manipolazione di buffer-undo-list funziona se sto usando undo-tree? Vedo che si fa riferimento nella fonte undo-tree, quindi suppongo di sì, ma dare un senso all'intera modalità sarebbe un grande sforzo.
Joseph Garvin,

@JosephGarvin Mi interessa anche controllare Emacs con la parola. Hai qualche fonte disponibile?
PythonNut,

@PythonNut: sì :) github.com/jgarvin/mandimus la confezione è incompleta ... e anche il codice è parzialmente nel mio repository joe-etc: p Ma lo uso tutto il giorno e funziona.
Joseph Garvin,

Risposte:


13

È interessante notare che non sembra esserci alcuna funzione integrata per farlo.

Il seguente codice funziona inserendo un marcatore univoco buffer-undo-listall'inizio di un blocco comprimibile e rimuovendo tutti i confini ( nilelementi) alla fine di un blocco, quindi rimuovendo il marcatore. Nel caso in cui qualcosa vada storto, il marcatore è del modulo (apply identity nil)per garantire che non faccia nulla se rimane nell'elenco di annullamento.

Idealmente, dovresti usare la with-undo-collapsemacro, non le funzioni sottostanti. Dato che hai detto che non puoi eseguire il wrapping, assicurati di passare ai marker di funzioni di basso livello che sono eq, non solo equal.

Se il codice invocato scambia i buffer, è necessario assicurarsi che undo-collapse-endvenga chiamato nello stesso buffer di undo-collapse-begin. In tal caso, verranno compresse solo le voci di annullamento nel buffer iniziale.

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

Ecco un esempio di utilizzo:

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

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

Capisco perché il tuo marker è un nuovo elenco, ma c'è un motivo per quegli elementi specifici?
Malabarba,

@Malabarba è perché una voce (apply identity nil)non farà nulla se la chiami primitive-undo- non romperà nulla se per qualche motivo viene lasciata nella lista.
Jch

Aggiornato la mia domanda per includere il codice che ho aggiunto. Grazie!
Joseph Garvin,

Qualche motivo da fare (eq (cadr l) nil)invece di (null (cadr l))?
ideasman42

@ ideasman42 modificato secondo il tuo suggerimento.
jch

3

Alcune modifiche al meccanismo di annullamento "recentemente" hanno rotto un hack che viper-modestava usando per fare questo tipo di collasso (per i curiosi, è usato nel seguente caso: quando si preme ESCper terminare un inserimento / sostituzione / edizione, Viper vuole far crollare il tutto cambiare in un singolo passaggio di annullamento).

Per risolverlo in modo chiaro, abbiamo introdotto una nuova funzione undo-amalgamate-change-group(che corrisponde più o meno alla tua undo-stop-collapsing) e riutilizza l'esistente prepare-change-groupper segnare l'inizio (cioè corrisponde più o meno alla tua undo-start-collapsing).

Per riferimento, ecco il nuovo codice Viper corrispondente:

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

Questa nuova funzione apparirà in Emacs-26, quindi se vuoi usarla nel frattempo, puoi copiarne la definizione (richiede 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))))))))

Ho esaminato undo-amalgamate-change-groupe non sembra esserci un modo conveniente per usarlo come la with-undo-collapsemacro definita in questa pagina, poiché atomic-change-groupnon funziona in un modo che consenta di chiamare il gruppo undo-amalgamate-change-group.
ideasman42

Certo, non lo usi con atomic-change-group: lo usi con prepare-change-group, che restituisce la maniglia a cui poi devi passare undo-amalgamate-change-groupquando hai finito.
Stefan,

Una macro che si occupa di questo non sarebbe utile? (with-undo-amalgamate ...)che gestisce le cose del gruppo di modifica. Altrimenti questo è un po 'una seccatura per il collasso di alcune operazioni.
ideasman42

Finora è utilizzato solo da Viper IIRC e Viper non sarebbe in grado di utilizzare una tale macro perché le due chiamate avvengono in comandi separati, quindi non è necessario piangerlo. Ma sarebbe banale scrivere una tale macro, ovviamente.
Stefan,

1
Questa macro potrebbe essere scritta e inclusa in emacs? Mentre per uno sviluppatore esperto è banale, per qualcuno che vuole comprimere la propria cronologia degli annullamenti e non sa da dove cominciare - è un po 'di tempo a fare casino online e inciampare su questo thread ... quindi dover scoprire quale risposta è la migliore - quando non sono abbastanza esperti da poterlo dire. Ho aggiunto una risposta qui: emacs.stackexchange.com/a/54412/2418
ideasman42

2

Ecco una with-undo-collapsemacro che utilizza la funzionalità dei gruppi di modifiche di Emacs-26.

Questo è atomic-change-groupcon un cambio di una riga, aggiungendo undo-amalgamate-change-group.

Ha i vantaggi che:

  • Non è necessario manipolare direttamente i dati di annullamento.
  • Assicura che i dati di annullamento non vengano troncati.
(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))))))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.