Bagaimana cara menghapus riwayat?

17

Saya sedang mengerjakan mode Emacs yang memungkinkan Anda mengontrol Emacs dengan pengenalan suara. Salah satu masalah yang saya temui adalah cara Emacs menangani undo tidak cocok dengan yang Anda harapkan berfungsi saat mengendalikan dengan suara.

Ketika pengguna mengucapkan beberapa kata dan kemudian berhenti, itu disebut 'ucapan.' Ucapan dapat terdiri dari beberapa perintah untuk dijalankan oleh Emacs. Seringkali pengenal mengenali satu atau lebih perintah dalam ucapan yang salah. Pada titik itu saya ingin bisa mengatakan "undo" dan minta Emacs membatalkan semua tindakan yang dilakukan oleh ujaran, bukan hanya tindakan terakhir dalam ujaran. Dengan kata lain, saya ingin Emacs memperlakukan ucapan sebagai satu perintah sejauh yang berkaitan dengan undo, bahkan ketika ucapan terdiri dari banyak perintah. Saya juga ingin poin untuk kembali ke tempat persisnya sebelum ujaran, saya perhatikan Emacs normal membatalkan tidak melakukan ini.

Saya memiliki pengaturan Emacs untuk mendapatkan panggilan balik di awal dan akhir setiap ucapan, sehingga saya dapat mendeteksi situasinya, saya hanya perlu mencari tahu apa yang harus dilakukan Emacs. Idealnya saya akan memanggil sesuatu seperti (undo-start-collapsing)dan kemudian (undo-stop-collapsing)dan apa pun yang dilakukan antara akan secara ajaib runtuh menjadi satu rekaman.

Saya melakukan beberapa penjelajahan melalui dokumentasi dan menemukan undo-boundary, tetapi itu kebalikan dari apa yang saya inginkan - saya perlu menciutkan semua tindakan dalam ucapan menjadi satu rekaman undo, tidak membaginya. Saya dapat menggunakan undo-boundaryantara ucapan untuk memastikan bahwa penyisipan dianggap terpisah (Emacs secara default menganggap tindakan penyisipan berurutan menjadi satu tindakan hingga batas tertentu), tetapi hanya itu.

Komplikasi lain:

  • Daemon pengenalan ucapan saya mengirim beberapa perintah ke Emacs dengan mensimulasikan penekanan tombol X11 dan mengirimkan beberapa melalui emacsclient -ebegitu, jika ada yang mengatakan (undo-collapse &rest ACTIONS)tidak ada tempat pusat yang bisa saya bungkus.
  • Saya menggunakan undo-tree, tidak yakin apakah ini membuat segalanya lebih rumit. Idealnya solusi akan bekerja dengan undo-treedan perilaku undo normal Emacs.
  • Bagaimana jika salah satu perintah dalam ucapan adalah "undo" atau "redo"? Saya berpikir saya bisa mengubah logika panggilan balik untuk selalu mengirim ini ke Emacs sebagai ucapan berbeda untuk menjaga hal-hal yang lebih sederhana, maka itu harus ditangani sama seperti jika saya menggunakan keyboard.
  • Regangkan tujuan: Ucapan dapat berisi perintah yang mengganti jendela atau buffer yang aktif. Dalam hal ini tidak apa-apa untuk mengatakan "undo" satu kali secara terpisah di setiap buffer, saya tidak perlu seperti itu. Tetapi semua perintah dalam buffer tunggal masih harus dikelompokkan, jadi jika saya mengatakan "do-x do-y do-z switch-buffer do-a do-b do-c" maka x, y, z harus menjadi satu catatan dalam buffer asli dan a, b, c harus menjadi satu catatan di switched ke buffer.

Apakah ada cara mudah untuk melakukan ini? AFAICT tidak ada built-in tapi Emacs luas dan dalam ...

Pembaruan: Saya akhirnya menggunakan solusi jhc di bawah ini dengan sedikit kode tambahan. Dalam global before-change-hooksaya memeriksa apakah buffer yang diubah ada dalam daftar global buffer yang dimodifikasi ucapan ini, jika tidak masuk ke dalam daftar dan undo-collapse-begindipanggil. Kemudian pada akhir ucapan saya mengulangi semua buffer dalam daftar dan panggilan undo-collapse-end. Kode di bawah ini (md- ditambahkan sebelum nama fungsi untuk keperluan penempatan nama):

(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
sumber
Tidak mengetahui adanya mekanisme bawaan untuk ini. Anda mungkin dapat memasukkan entri Anda sendiri ke buffer-undo-listsebagai penanda - mungkin entri formulir (apply FUN-NAME . ARGS)? Kemudian untuk membatalkan ucapan Anda berulang kali menelepon undosampai menemukan penanda Anda berikutnya. Tapi saya curiga ada banyak jenis komplikasi di sini. :)
glucas
Menghapus batas tampaknya merupakan taruhan yang lebih baik.
jch
Apakah memanipulasi buffer-undo-list berfungsi jika saya menggunakan undo-tree? Saya melihatnya direferensikan dalam sumber undo-tree jadi saya kira ya tapi masuk akal seluruh mode akan menjadi upaya besar.
Joseph Garvin
@ JosephephGarvin Saya tertarik mengendalikan Emacs dengan pidato juga. Apakah Anda memiliki sumber yang tersedia?
PythonNut
@PythonNut: yes :) github.com/jgarvin/mandimus kemasannya tidak lengkap ... dan kode ini juga sebagian di repo-joe-etc: p Tapi saya menggunakannya sepanjang hari dan berfungsi.
Joseph Garvin

Jawaban:

13

Yang cukup menarik, tampaknya tidak ada fungsi bawaan untuk melakukan itu.

Kode berikut berfungsi dengan menyisipkan penanda unik pada buffer-undo-listdi awal blok yang dapat dilipat, dan menghapus semua batas ( nilelemen) di ujung blok, kemudian menghapus penanda. Dalam hal terjadi kesalahan, penanda berbentuk (apply identity nil)untuk memastikan bahwa ia tidak melakukan apa-apa jika tetap pada daftar undo.

Idealnya, Anda harus menggunakan with-undo-collapsemakro, bukan fungsi yang mendasarinya. Karena Anda menyebutkan bahwa Anda tidak dapat melakukan pembungkus, pastikan bahwa Anda beralih ke penanda fungsi tingkat rendah eq, bukan hanya equal.

Jika kode yang dipanggil mengganti buffer, Anda harus memastikan bahwa undo-collapse-enddipanggil dalam buffer yang sama dengan undo-collapse-begin. Dalam hal ini, hanya entri yang dibatalkan di buffer awal yang akan diciutkan.

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

Berikut ini contoh penggunaan:

(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
sumber
Saya mengerti mengapa marker Anda adalah daftar baru, tetapi apakah ada alasan untuk elemen-elemen spesifik itu?
Malabarba
@ Malabarba itu karena sebuah entri (apply identity nil)tidak akan melakukan apa-apa jika Anda memanggilnya primitive-undo- itu tidak akan merusak apa pun jika karena alasan tertentu dibiarkan dalam daftar.
jch
Memperbarui pertanyaan saya untuk memasukkan kode yang saya tambahkan. Terima kasih!
Joseph Garvin
Alasan untuk melakukan (eq (cadr l) nil)bukan (null (cadr l))?
ideasman42
@ ideasman42 dimodifikasi sesuai dengan saran Anda.
jch
3

Beberapa perubahan pada mesin undo "baru-baru ini" memecahkan beberapa hack viper-modeyang digunakan untuk melakukan collapsing semacam ini (untuk yang penasaran, ini digunakan dalam kasus berikut: ketika Anda menekan ESCuntuk menyelesaikan penyisipan / penggantian / edisi, Viper ingin menutup keseluruhan ubah menjadi satu langkah undo).

Untuk memperbaikinya dengan bersih, kami memperkenalkan fungsi baru undo-amalgamate-change-group(yang sesuai dengan Anda atau kurang undo-stop-collapsing) dan menggunakan yang sudah ada prepare-change-groupuntuk menandai awal (yaitu sesuai lebih atau kurang untuk Anda undo-start-collapsing).

Untuk referensi, inilah kode Viper baru yang sesuai:

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

Fungsi baru ini akan muncul di Emacs-26, jadi jika Anda ingin menggunakannya dalam waktu yang bersamaan, Anda dapat menyalin definisi (memerlukan 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
sumber
Saya melihat ke dalam undo-amalgamate-change-group, dan sepertinya tidak ada cara yang nyaman untuk menggunakan ini seperti with-undo-collapsemakro yang didefinisikan pada halaman ini, karena atomic-change-grouptidak berfungsi dengan cara yang memungkinkan memanggil grup dengan undo-amalgamate-change-group.
ideasman42
Tentu saja, Anda tidak menggunakannya dengan atomic-change-group: Anda menggunakannya dengan prepare-change-group, yang mengembalikan gagang yang Anda perlukan untuk dilewati undo-amalgamate-change-groupketika Anda selesai.
Stefan
Bukankah makro yang berurusan dengan ini bermanfaat? (with-undo-amalgamate ...)yang menangani hal-hal perubahan grup. Kalau tidak, ini sedikit merepotkan untuk runtuh beberapa operasi.
ideasman42
Sejauh ini hanya digunakan oleh viper IIRC dan Viper tidak akan dapat menggunakan makro seperti itu karena dua panggilan terjadi dalam perintah terpisah, jadi tidak perlu menangis untuk itu. Tapi tentu sepele untuk menulis makro seperti itu, tentu saja.
Stefan
1
Bisakah makro ini ditulis dan dimasukkan dalam emacs? Sementara untuk pengembang yang berpengalaman itu sepele, bagi seseorang yang ingin menutup riwayat undo mereka dan tidak tahu harus mulai dari mana - kadang-kadang main-main online dan tersandung pada utas ini ... kemudian harus mencari tahu jawaban mana yang terbaik - ketika mereka tidak cukup berpengalaman untuk bisa tahu. Saya menambahkan jawaban di sini: emacs.stackexchange.com/a/54412/2418
ideasman42
2

Ini adalah with-undo-collapsemakro yang menggunakan fitur grup perubahan Emacs-26.

Ini atomic-change-groupdengan satu perubahan baris, menambahkan undo-amalgamate-change-group.

Ini memiliki kelebihan yaitu:

  • Tidak perlu memanipulasi data undo secara langsung.
  • Ini memastikan undo data tidak terpotong.
(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))))))
gagasanman42
sumber