Я работаю над режимом Emacs, который позволяет вам управлять Emacs с помощью распознавания речи. Одна из проблем, с которыми я столкнулся, заключается в том, что способ обработки отмены Emacs не соответствует тому, как вы ожидаете, что он будет работать при управлении голосом.
Когда пользователь говорит несколько слов, а затем делает паузу, это называется «высказывание». Выражение может состоять из нескольких команд для выполнения Emacs. Часто случается, что распознаватель распознает одну или несколько команд в высказывании неправильно. В этот момент я хочу иметь возможность сказать «отменить» и заставить Emacs отменить все действия, выполняемые высказыванием, а не только последнее действие в высказывании. Другими словами, я хочу, чтобы Emacs рассматривал высказывание как отдельную команду, если речь идет об отмене, даже когда высказывание состоит из нескольких команд. Я также хотел бы вернуться к тому, что было до высказывания, я заметил, что обычная отмена Emacs этого не делает.
Я настроил Emacs для получения обратных вызовов в начале и в конце каждого высказывания, поэтому я могу определить ситуацию, мне просто нужно выяснить, что должен делать Emacs. В идеале я бы назвал что-то вроде этого, (undo-start-collapsing)
и тогда (undo-stop-collapsing)
все, что было сделано между ними, было бы волшебным образом свернуто в одну запись.
Я немного просмотрел документацию и нашел undo-boundary
, но это противоположно тому, что я хочу - мне нужно объединить все действия внутри высказывания в одну запись отмены, а не разделить их. Я могу использовать undo-boundary
между высказываниями, чтобы убедиться, что вставки считаются отдельными (Emacs по умолчанию считает последовательные действия вставки одним действием до определенного предела), но это так.
Другие осложнения:
- Мой демон распознавания речи отправляет некоторые команды в Emacs, имитируя нажатия клавиш X11, и отправляет некоторые через,
emacsclient -e
так что, если бы, скажем(undo-collapse &rest ACTIONS)
, не было центрального места, которое я мог бы обернуть. - Я пользуюсь
undo-tree
, не уверен, что это усложнит ситуацию. В идеале решение должно работать сundo-tree
нормальным поведением отмены Emacs. - Что если одна из команд в высказывании - «отменить» или «повторить»? Я думаю, что я мог бы изменить логику обратного вызова, чтобы всегда отправлять их в Emacs как отдельные высказывания, чтобы упростить задачу, тогда это должно быть обработано так же, как если бы я использовал клавиатуру.
- Цель растяжения: высказывание может содержать команду, которая переключает текущее активное окно или буфер. В этом случае хорошо бы сказать «отменить» по одному разу в каждом буфере, мне не нужно, чтобы это было так красиво. Но все команды в одном буфере все равно должны быть сгруппированы, поэтому, если я скажу «do-x do-y do-z switch-buffer do-a do-b do-c», то x, y, z должны быть на одну отмену запись в исходном буфере и a, b, c должна быть одной записью в переключенном буфере.
Есть простой способ сделать это? AFAICT нет ничего встроенного, но Emacs обширный и глубокий ...
Обновление: я закончил тем, что использовал решение JHC ниже с небольшим дополнительным кодом. В глобальном before-change-hook
я проверяю, находится ли изменяемый буфер в глобальном списке буферов, модифицированных этим высказыванием, если нет, он входит в список и undo-collapse-begin
вызывается. Затем в конце высказывания я перебираю все буферы в списке и вызываю undo-collapse-end
. Код ниже (md - добавлен перед именами функций для целей именования):
(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
качестве маркера - возможно, запись в форме(apply FUN-NAME . ARGS)
? Затем, чтобы отменить высказывание, вы неоднократно звоните,undo
пока не найдете свой следующий маркер. Но я подозреваю, что здесь есть все виды осложнений. :)Ответы:
Интересно, что для этого не существует встроенной функции.
Следующий код работает, вставляя уникальный маркер
buffer-undo-list
в начале свертываемого блока, удаляя все границы (nil
элементы) в конце блока, а затем удаляя маркер. Если что-то идет не так, маркер имеет форму,(apply identity nil)
чтобы гарантировать, что он ничего не делает, если он остается в списке отмены.В идеале вы должны использовать
with-undo-collapse
макрос, а не основные функции. Поскольку вы упомянули, что вы не можете выполнять обтекание, убедитесь, что вы переходите к низкоуровневым маркерам функцийeq
, а не только к нимequal
.Если вызванный код переключает буферы, вы должны убедиться, что он
undo-collapse-end
вызывается в том же буфере, что иundo-collapse-begin
. В этом случае будут свернуты только записи отмены в начальном буфере.Вот пример использования:
источник
(apply identity nil)
ничего не сделает, если выprimitive-undo
к ней обращаетесь - она ничего не сломает, если по какой-то причине она останется в списке.(eq (cadr l) nil)
вместо(null (cadr l))
?Некоторые изменения в механизме отмены «недавно» сломали некоторые хакерские программы,
viper-mode
использовавшиеся для этого вида свертывания (любопытно, что он используется в следующем случае: когда вы нажимаете, ESCчтобы завершить вставку / замену / издание, Viper хочет свернуть все изменить в один шаг отмены).Чтобы исправить это, мы ввели новую функцию
undo-amalgamate-change-group
(которая более или менее соответствует вашейundo-stop-collapsing
) и повторно используют существующую,prepare-change-group
чтобы отметить начало (т. Е. Более или менее соответствует вашейundo-start-collapsing
).Для справки вот соответствующий новый код Viper:
Эта новая функция появится в Emacs-26, поэтому, если вы хотите использовать ее в то же время, вы можете скопировать ее определение (обязательно
cl-lib
):источник
undo-amalgamate-change-group
, и, кажется, нет удобного способа использовать это, какwith-undo-collapse
макрос, определенный на этой странице, такatomic-change-group
как не работает таким образом, который позволяет вызывать группу сundo-amalgamate-change-group
.atomic-change-group
: вы используете это сprepare-change-group
, который возвращает дескриптор, который вам нужно передать,undo-amalgamate-change-group
когда вы закончите.(with-undo-amalgamate ...)
который обрабатывает изменения группы вещей. В противном случае это немного хлопотно для свертывания нескольких операций.Вот
with-undo-collapse
макрос, который использует функцию групп изменений Emacs-26.Это
atomic-change-group
с одной строкой изменения, добавлениеundo-amalgamate-change-group
.Он имеет следующие преимущества:
источник