Làm thế nào để sụp đổ hoàn tác lịch sử?


17

Tôi đang làm việc ở chế độ Emacs cho phép bạn điều khiển Emacs bằng nhận dạng giọng nói. Một trong những vấn đề tôi gặp phải là cách Emacs xử lý hoàn tác không khớp với cách bạn mong đợi nó hoạt động khi điều khiển bằng giọng nói.

Khi người dùng nói vài từ và sau đó tạm dừng, đó được gọi là 'cách nói'. Một cách nói có thể bao gồm nhiều lệnh để Emacs thực thi. Đó thường là trường hợp bộ nhận dạng nhận ra một hoặc nhiều lệnh trong cách phát âm không chính xác. Tại thời điểm đó, tôi muốn có thể nói "hoàn tác" và yêu cầu Emacs hoàn tác mọi hành động được thực hiện bằng cách nói, không chỉ là hành động cuối cùng trong cách nói. Nói cách khác, tôi muốn Emacs coi một cách nói như một lệnh duy nhất khi có liên quan đến việc hoàn tác, ngay cả khi một cách nói bao gồm nhiều lệnh. Tôi cũng muốn quay trở lại chính xác vị trí của nó trước khi phát ngôn, tôi nhận thấy Emacs bình thường hoàn tác không làm điều này.

Tôi đã thiết lập Emacs để nhận được các cuộc gọi lại ở đầu và cuối của mỗi cách nói, vì vậy tôi có thể phát hiện tình huống, tôi chỉ cần tìm ra những gì để Emacs làm. Lý tưởng nhất là tôi gọi một cái gì đó giống như (undo-start-collapsing)và sau đó (undo-stop-collapsing)và bất cứ điều gì được thực hiện ở giữa sẽ được thu gọn một cách kỳ diệu vào một bản ghi.

Tôi đã thực hiện một số bước qua tài liệu và tìm thấy undo-boundary, nhưng nó trái ngược với những gì tôi muốn - tôi cần thu gọn tất cả các hành động trong một phát ngôn thành một bản ghi hoàn tác, không tách chúng ra. Tôi có thể sử dụng undo-boundarygiữa các cách nói để đảm bảo các phần chèn thêm được coi là riêng biệt (Emacs theo mặc định coi các hành động chèn liên tiếp là một hành động đến một giới hạn nào đó), nhưng đó là.

Các biến chứng khác:

  • Trình nền nhận dạng giọng nói của tôi gửi một số lệnh tới Emacs bằng cách mô phỏng các phím bấm X11 và gửi một số thông qua emacsclient -e, nếu có nói rằng (undo-collapse &rest ACTIONS)không có vị trí trung tâm nào tôi có thể bọc.
  • Tôi sử dụng undo-tree, không chắc chắn nếu điều này làm cho mọi thứ phức tạp hơn. Lý tưởng nhất là một giải pháp sẽ hoạt động với undo-treevà hành vi hoàn tác bình thường của Emacs.
  • Điều gì xảy ra nếu một trong các lệnh trong cách nói là "hoàn tác" hoặc "làm lại"? Tôi nghĩ rằng tôi có thể thay đổi logic gọi lại để luôn gửi chúng cho Emacs như những cách nói riêng biệt để giữ mọi thứ đơn giản hơn, sau đó nó sẽ được xử lý giống như khi tôi sử dụng bàn phím.
  • Mục tiêu kéo dài: Một cách nói có thể chứa một lệnh chuyển cửa sổ hoặc bộ đệm hiện đang hoạt động. Trong trường hợp này, thật tốt khi phải nói "hoàn tác" một lần riêng biệt trong mỗi bộ đệm, tôi không cần nó trở nên lạ mắt. Nhưng tất cả các lệnh trong một bộ đệm vẫn phải được nhóm lại, vì vậy nếu tôi nói "do-x do-y do-z switch-buffer do-a do-b do-c" thì x, y, z sẽ là một hoàn tác bản ghi trong bộ đệm ban đầu và a, b, c phải là một bản ghi trong bộ đệm được chuyển sang bộ đệm.

Có cách nào làm dễ hơn không? AFAICT không có gì tích hợp nhưng Emacs rộng lớn và sâu sắc ...

Cập nhật: Tôi đã kết thúc bằng cách sử dụng giải pháp của jhc bên dưới với một chút mã bổ sung. Trong toàn cầu, before-change-hooktôi kiểm tra xem bộ đệm đang được thay đổi có trong danh sách bộ đệm toàn cầu đã sửa đổi cách phát âm này hay không, nếu không nó sẽ đi vào danh sách và undo-collapse-beginđược gọi. Sau đó, khi kết thúc câu nói, tôi lặp lại tất cả các bộ đệm trong danh sách và gọi undo-collapse-end. Mã bên dưới (md- được thêm trước tên hàm cho mục đích đặt tên):

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

Không biết về một cơ chế tích hợp cho việc này. Bạn có thể chèn các mục nhập của riêng bạn vào buffer-undo-listdưới dạng điểm đánh dấu - có lẽ là mục nhập của biểu mẫu (apply FUN-NAME . ARGS)? Sau đó để hoàn tác một cách nói bạn liên tục gọi undocho đến khi tìm thấy điểm đánh dấu tiếp theo của bạn. Nhưng tôi nghi ngờ có tất cả các loại biến chứng ở đây. :)
glucas

Xóa bỏ ranh giới có vẻ là một đặt cược tốt hơn.
JCH

Thao tác bộ đệm hoàn tác có hoạt động không nếu tôi đang sử dụng undo-tree? Tôi thấy nó được tham chiếu trong nguồn hoàn tác cây nên tôi đoán là có nhưng ý nghĩa của toàn bộ chế độ sẽ là một nỗ lực lớn.
Joseph Garvin

@JosephGarvin Tôi cũng thích kiểm soát Emacs bằng lời nói. Bạn có nguồn nào không?
PythonNut

@PythonNut: có :) github.com/jgarvin/mandimus bao bì không đầy đủ ... và mã cũng là một phần trong repo joe-etc của tôi: p Nhưng tôi sử dụng nó cả ngày và nó hoạt động.
Joseph Garvin

Câu trả lời:


13

Thật thú vị, dường như không có chức năng tích hợp để làm điều đó.

Đoạn mã sau hoạt động bằng cách chèn một điểm đánh dấu duy nhất vào buffer-undo-listđầu khối đóng mở và xóa tất cả các ranh giới ( nilphần tử) ở cuối một khối, sau đó xóa điểm đánh dấu. Trong trường hợp có lỗi xảy ra, điểm đánh dấu có dạng (apply identity nil)để đảm bảo rằng nó không làm gì nếu nó vẫn nằm trong danh sách hoàn tác.

Tốt nhất, bạn nên sử dụng with-undo-collapsemacro, không phải các chức năng cơ bản. Vì bạn đã đề cập rằng bạn không thể thực hiện gói, hãy đảm bảo rằng bạn chuyển đến các điểm đánh dấu chức năng cấp thấp eq, không chỉ equal.

Nếu mã được gọi chuyển đổi bộ đệm, bạn phải đảm bảo rằng nó undo-collapse-endđược gọi trong cùng bộ đệm như undo-collapse-begin. Trong trường hợp đó, chỉ các mục hoàn tác trong bộ đệm ban đầu sẽ được thu gọn.

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

Đây là một ví dụ về việc sử dụng:

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

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

Tôi hiểu tại sao điểm đánh dấu của bạn là một danh sách mới, nhưng có lý do cho những yếu tố cụ thể đó không?
Malabarba

@Malabarba đó là vì một mục nhập (apply identity nil)sẽ không làm gì nếu bạn gọi primitive-undonó - nó sẽ không phá vỡ bất cứ điều gì nếu vì lý do nào đó nó bị bỏ lại trong danh sách.
JCH

Cập nhật câu hỏi của tôi để bao gồm mã tôi đã thêm. Cảm ơn!
Joseph Garvin

Bất kỳ lý do để làm (eq (cadr l) nil)thay vì (null (cadr l))?
ideaman42

@ ideaman42 sửa đổi theo đề nghị của bạn.
JCH

3

Một số thay đổi đối với máy hoàn tác "gần đây" đã phá vỡ một số hack viper-modeđã sử dụng để thực hiện loại sụp đổ này (đối với người tò mò, nó được sử dụng trong trường hợp sau: khi bạn nhấn ESCđể hoàn tất việc chèn / thay thế / phiên bản, Viper muốn thu gọn toàn bộ thay đổi thành một bước hoàn tác duy nhất).

Để khắc phục nó một cách sạch sẽ, chúng tôi đã giới thiệu một chức năng mới undo-amalgamate-change-group(tương ứng ít nhiều với bạn undo-stop-collapsing) và sử dụng lại chức năng hiện có prepare-change-groupđể đánh dấu sự bắt đầu (nghĩa là tương ứng nhiều hơn hoặc ít hơn với bạn undo-start-collapsing).

Để tham khảo, đây là mã Viper mới tương ứng:

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

Hàm mới này sẽ xuất hiện trong Emacs-26, vì vậy nếu bạn muốn sử dụng nó trong thời gian trung bình, bạn có thể sao chép định nghĩa của nó (yêu cầu 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))))))))

Tôi đã xem xét undo-amalgamate-change-groupvà dường như không có cách nào thuận tiện để sử dụng điều này giống như with-undo-collapsemacro được xác định trên trang này, vì atomic-change-groupnó không hoạt động theo cách cho phép gọi nhóm bằng undo-amalgamate-change-group.
ideaman42

Tất nhiên, bạn không sử dụng nó với atomic-change-group: bạn sử dụng nó với prepare-change-group, nó trả về tay cầm mà bạn đã chuyển đến undo-amalgamate-change-groupkhi bạn hoàn thành.
Stefan

Sẽ không một macro liên quan đến điều này sẽ hữu ích? (with-undo-amalgamate ...)trong đó xử lý các công cụ nhóm thay đổi. Nếu không, đây là một chút rắc rối cho việc thu gọn một vài thao tác.
ideaman42

Cho đến nay, nó chỉ được sử dụng bởi viper IIRC và Viper sẽ không thể sử dụng macro như vậy bởi vì hai cuộc gọi xảy ra trong các lệnh riêng biệt, do đó không cần phải khóc cho nó. Nhưng nó sẽ là tầm thường để viết một macro như vậy, tất nhiên.
Stefan

1
Macro này có thể được viết và bao gồm trong emacs không? Trong khi đối với một nhà phát triển có kinh nghiệm thì nó không quan trọng, đối với một người muốn thu gọn lịch sử hoàn tác của họ và không biết bắt đầu từ đâu - thì đôi khi phải loay hoay trên mạng và vấp phải chủ đề này ... sau đó phải tìm ra câu trả lời nào là tốt nhất - khi họ không đủ kinh nghiệm để có thể nói. Tôi đã thêm một câu trả lời ở đây: emacs.stackexchange.com/a/54412/2418
ideaman42

2

Đây là một with-undo-collapsemacro sử dụng tính năng nhóm thay đổi Emacs-26.

Đây là atomic-change-groupvới một thay đổi một dòng, thêm undo-amalgamate-change-group.

Nó có những ưu điểm:

  • Nó không cần phải thao tác trực tiếp với dữ liệu.
  • Nó đảm bảo hoàn tác dữ liệu không bị cắt ngắn.
(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))))))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.