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-boundary
tra 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 -e
tale, 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 conundo-tree
il 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-hook
controllo se il buffer che si sta modificando è in un elenco globale di buffer modificato questa espressione, se non va nell'elenco e undo-collapse-begin
viene 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)
buffer-undo-list
come indicatore - forse una voce del modulo(apply FUN-NAME . ARGS)
? Quindi per annullare un enunciato, chiedi ripetutamenteundo
fino a trovare il tuo prossimo marker. Ma sospetto che ci siano tutti i tipi di complicazioni qui. :)