元に戻す履歴を折りたたむ方法は?


17

私は、音声認識でEmacsを制御できるEmacsモードに取り組んでいます。私が遭遇した問題の1つは、Emacsが元に戻す操作を処理する方法が、音声で制御するときの動作の期待と一致しないことです。

ユーザーがいくつかの単語を話してから一時停止すると、それは「発話」と呼ばれます。発話は、Emacsが実行する複数のコマンドで構成される場合があります。多くの場合、認識エンジンが発話内の1つ以上のコマンドを誤って認識します。その時点で、「元に戻す」と言って、発話内の最後のアクションだけでなく、発話によって行われたすべてのアクションをEmacsで元に戻すことができます。言い換えれば、発話が複数のコマンドで構成されている場合でも、アンドゥに関する限り、Emacsが発話を単一のコマンドとして扱うようにします。また、発話前の正確な場所に戻りたいのですが、通常のEmacsの取り消しではこれが行われないことに気付きました。

各発言の最初と最後にコールバックを取得するようにEmacsをセットアップしているので、状況を検出できます。Emacsに何をさせるかを把握する必要があります。理想的には私のような何かを呼びたい(undo-start-collapsing)し、その後(undo-stop-collapsing)、何完了挟んは魔法のように一つのレコードにまとめられます。

ドキュメントをいくつか探して見つけましたundo-boundaryが、それは私が望むものの反対です-発話内のすべてのアクションを、それらを分割せずに1つのアンドゥレコードに折りたたむ必要があります。undo-boundary発話間で使用して、挿入が個別と見なされるようにすることができます(Emacsは、デフォルトでは連続する挿入アクションをある制限までの1つのアクションと見なします)が、それだけです。

その他の合併症:

  • 私の音声認識デーモンは、X11キー押下をシミュレートすることによっていくつかのコマンドをEmacsに送信し、いくつかのコマンドをemacsclient -eそのように送信します(undo-collapse &rest ACTIONS)
  • を使用しますがundo-tree、これにより事態がさら​​に複雑になるかどうかはわかりません。理想的には、ソリューションはundo-treeEmacsの通常のアンドゥ動作で動作します。
  • 発話内のコマンドの1つが「元に戻す」または「やり直し」の場合はどうなりますか?コールバックロジックを変更して、これらを常に個別の発話としてEmacsに送信して、物事をよりシンプルに保つことができると考えています。キーボードを使用する場合と同じように処理する必要があります。
  • ストレッチ目標:発話には、現在アクティブなウィンドウまたはバッファを切り替えるコマンドが含まれる場合があります。この場合、各バッファで個別に「元に戻す」と言う必要がありますが、それほど派手である必要はありません。ただし、単一のバッファー内のすべてのコマンドはグループ化する必要があります。したがって、「do-x do-y do-zスイッチバッファーdo-a do-b do-c」と言うと、x、y、zは1つの取り消しになります。元のバッファのレコードとa、b、cは、バッファに切り替えられた1つのレコードでなければなりません。

これを行う簡単な方法はありますか?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、次のマーカーが見つかるまで繰り返し呼び出す発話を元に戻します。しかし、ここにはあらゆる種類の合併症があると思います。:)
グルーカス

境界線を削除する方が良いと思われます。
jch

undo-treeを使用している場合、buffer-undo-listの操作は機能しますか?元に戻すツリーのソースで参照されているので、「はい」と推測していますが、モード全体を理解することは大きな努力です。
ジョセフガービン

@JosephGarvin私もスピーチでEmacsを制御することに興味があります。利用可能なソースはありますか?
PythonNut

@PythonNut:yes :) github.com/jgarvin/mandimusパッケージングは​​不完全です...そしてコードは私のジョーなどのレポにも部分的に含まれています:
ジョセフガービン

回答:


13

興味深いことに、それを行うための組み込み関数はないようです。

次のコードbuffer-undo-listは、折りたたみ可能なブロックの先頭に一意のマーカーを挿入し、ブロックnilの末尾にあるすべての境界(要素)を削除してからマーカーを削除することで機能します。何か問題が発生した場合、マーカーは(apply identity nil)元に戻すリストに残っている場合は何もしないことを保証する形式です。

理想的にwith-undo-collapseは、基礎となる関数ではなく、マクロを使用する必要があります。あなたはラッピングを行うことができないことに言及しているので、あなたは低レベル関数のマーカーに渡すことを確認してくださいeqだけではなくequal

呼び出されたコードがバッファを切り替える場合は、それundo-collapse-endがと同じバッファで呼び出されることを確認する必要がありますundo-collapse-begin。その場合、初期バッファ内の元に戻すエントリのみが縮小されます。

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

使用例を次に示します。

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

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

マーカーが新しいリストである理由は理解していますが、これらの特定の要素には理由がありますか?
マラバルバ

@Malabarbaは、(apply identity nil)それを呼び出すとエントリがprimitive-undo何もしないためです。何らかの理由でリストに残っていても何も壊れません。
jch

追加したコードを含むように質問を更新しました。ありがとう!
ジョセフガービン

(eq (cadr l) nil)代わりに行う理由は(null (cadr l))何ですか?
ideasman42

@ ideasman42はあなたの提案に従って修正されました。
jch

3

元に戻す機構へのいくつかの変更は、「ハッキング」がviper-modeこの種の折りたたみを行うために使用していた「最近」を壊しました(奇妙なことにESC、挿入/置換/編集を押すために押すと、Viperは全体を折りたたみたいと思います単一の元に戻すステップに変更します)。

きれいに修正するために、新しい関数undo-amalgamate-change-group(にほぼ対応する)を導入しundo-stop-collapsing、既存のものprepare-change-groupを再利用して開始をマークします(つまり、にほぼ対応しますundo-start-collapsing)。

参考のために、対応する新しいViperコードを次に示します。

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

この新しい関数はEmacs-26に表示されるため、その間に使用したい場合は、その定義をコピーできます(必須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))))))))

私はに見えたundo-amalgamate-change-group、とのようにこれを使用するための便利な方法があるように思われないwith-undo-collapseため、このページで定義されたマクロatomic-change-groupを持つグループを呼び出すことができますように仕事をしませんundo-amalgamate-change-group
ideasman42

もちろん、あなたがそれを使用しないでくださいatomic-change-group:あなたがそれを使用prepare-change-groupしますが、その後に渡す定義さハンドル返す、undo-amalgamate-change-groupあなたが完了したらを。
ステファン

これを扱うマクロは役に立ちませんか?(with-undo-amalgamate ...)変更グループのスタッフを処理します。それ以外の場合、これはいくつかの操作を折りたたむのに少し面倒です。
ideasman42

これまでのところ、これはviper IIRCでのみ使用されており、2つの呼び出しは別々のコマンドで発生するため、Viperはそのようなマクロを使用できません。しかし、もちろんこのようなマクロを書くのは簡単です。
ステファン

1
このマクロを記述してemacsに含めることはできますか?経験豊富な開発者にとっては簡単なことですが、元に戻す履歴を折りたたんでどこから始めたらよいかわからない人にとっては、オンラインをいじってこのスレッドにつまずくのです。伝えることができるほど十分に経験していないとき。ここに答えを追加しました:emacs.stackexchange.com/a/54412/2418
ideasman42

2

これは、with-undo-collapseEmacs-26変更グループ機能を使用するマクロです。

これはatomic-change-group、を1行変更して追加しundo-amalgamate-change-groupます。

以下の利点があります。

  • 元に戻すデータを直接操作する必要はありません。
  • 元に戻すデータが切り捨てられないようにします。
(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))))))
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.