จะยุบประวัติเลิกทำอย่างไร


17

ฉันกำลังทำงานในโหมด Emacs ที่ให้คุณควบคุม Emacs ด้วยการรู้จำเสียง หนึ่งในปัญหาที่ฉันพบคือวิธีที่ Emacs จัดการเลิกทำไม่ตรงกับที่คุณคาดหวังว่ามันจะทำงานเมื่อควบคุมด้วยเสียง

เมื่อผู้ใช้พูดหลาย ๆ คำและหยุดชั่วคราวนั่นเรียกว่า 'คำพูด' คำพูดอาจประกอบด้วยหลายคำสั่งเพื่อให้ Emacs ดำเนินการ บ่อยครั้งที่ตัวจำแนกลายมือรู้จักหนึ่งคำสั่งหรือมากกว่านั้นภายในคำพูดที่ไม่ถูกต้อง ณ จุดนั้นฉันต้องการที่จะพูดว่า "เลิกทำ" และให้ Emacs เลิกทำการกระทำทั้งหมดด้วยคำพูดไม่ใช่แค่การกระทำสุดท้ายภายในคำพูด กล่าวอีกนัยหนึ่งฉันต้องการให้ Emacs ใช้คำพูดเดียวกับคำสั่งที่เกี่ยวข้องกับการเลิกทำแม้ว่าคำพูดนั้นจะประกอบด้วยหลายคำสั่งก็ตาม ฉันต้องการที่จะกลับไปที่เดิมก่อนคำพูดฉันสังเกตเห็นว่า Emacs การเลิกทำตามปกติไม่ได้ทำเช่นนี้

ฉันมีการตั้งค่า Emacs เพื่อรับการเรียกกลับที่จุดเริ่มต้นและจุดสิ้นสุดของคำพูดแต่ละคำเพื่อให้ฉันสามารถตรวจสอบสถานการณ์ได้ฉันต้องคิดว่าจะให้ Emacs ทำอะไร เป็นการดีที่ฉันจะเรียกสิ่งที่ชอบ(undo-start-collapsing)แล้ว(undo-stop-collapsing)และสิ่งที่ทำในระหว่างนั้นจะถูกยุบลงอย่างน่าอัศจรรย์เป็นหนึ่งระเบียน

ฉันทำการสืบค้นผ่านเอกสารและพบundo-boundaryแต่มันตรงกันข้ามกับสิ่งที่ฉันต้องการ - ฉันต้องยุบการกระทำทั้งหมดภายในคำพูดเป็นหนึ่งในการยกเลิกการบันทึกไม่ใช่การแยกพวกเขาออก ฉันสามารถใช้undo-boundaryระหว่างคำพูดเพื่อให้แน่ใจว่าการแทรกได้รับการพิจารณาแยกต่างหาก (โดยค่าเริ่มต้นจะพิจารณาการแทรกที่ต่อเนื่องกันเป็นหนึ่งการกระทำจนถึงขีด จำกัด ) แต่นั่นก็คือ

ภาวะแทรกซ้อนอื่น ๆ :

  • daemon การจดจำเสียงของฉันส่งคำสั่งไปยัง 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จนกระทั่งพบเครื่องหมายถัดไปของคุณ แต่ฉันสงสัยว่ามีภาวะแทรกซ้อนทุกประเภทที่นี่ :)
ลูกัส

การลบขอบเขตจะเป็นทางออกที่ดีกว่า
jch

การจัดการการยกเลิกบัฟเฟอร์รายการทำงานอย่างไรหากฉันใช้เลิกทำต้นไม้? ฉันเห็นว่ามีการอ้างอิงในแหล่งที่มาต้นไม้เลิกดังนั้นฉันเดาว่าใช่ แต่การทำความเข้าใจกับโหมดทั้งหมดจะเป็นความพยายามที่ยิ่งใหญ่
โจเซฟ Garvin

@JosephGarvin ฉันสนใจที่จะควบคุม Emac ด้วยคำพูดเช่นกัน คุณมีแหล่งข้อมูลใด ๆ หรือไม่?
PythonNut

@PythonNut: ใช่ :) github.com/jgarvin/mandimusบรรจุภัณฑ์ไม่สมบูรณ์ ... และรหัสก็เป็นส่วนหนึ่งใน repo joe-etc ของฉัน: p แต่ฉันใช้มันทั้งวันและใช้งานได้
โจเซฟการ์วิน

คำตอบ:


13

น่าสนใจพอดูเหมือนว่าไม่มีฟังก์ชั่นในตัวให้ทำ

รหัสต่อไปนี้ทำงานโดยการใส่เครื่องหมายเฉพาะbuffer-undo-listที่จุดเริ่มต้นของบล็อกที่ยุบได้และลบขอบเขต ( nilองค์ประกอบ) ทั้งหมดที่ส่วนท้ายของบล็อกจากนั้นนำเครื่องหมายออก ในกรณีที่มีข้อผิดพลาดตัวทำเครื่องหมายอยู่ในแบบฟอร์ม(apply identity nil)เพื่อให้แน่ใจว่าจะไม่ทำอะไรเลยหากยังคงอยู่ในรายการเลิกทำ

เป็นการดีที่คุณควรใช้with-undo-collapseแมโครไม่ใช่ฟังก์ชันพื้นฐาน เนื่องจากคุณบอกว่าคุณไม่สามารถทำห่อตรวจสอบให้แน่ใจว่าคุณส่งผ่านไปยังเครื่องหมายฟังก์ชั่นในระดับต่ำที่มีไม่เพียงeqequal

ถ้ารหัสเรียกสลับบัฟเฟอร์คุณต้องแน่ใจว่าจะเรียกว่าในบัฟเฟอร์เดียวกับ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

@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เมื่อคุณทำเสร็จแล้ว
Stefan

มาโครที่จัดการกับสิ่งนี้จะมีประโยชน์หรือไม่ (with-undo-amalgamate ...)ซึ่งจัดการกับสิ่งที่เปลี่ยนแปลงกลุ่ม ไม่เช่นนั้นจะเป็นเรื่องที่ยุ่งยากเล็กน้อยในการยุบการดำเนินการบางอย่าง
ideasman42

จนถึงตอนนี้มันถูกใช้โดย viper IIRC และ Viper เท่านั้นที่จะไม่สามารถใช้มาโครได้เนื่องจากการเรียกสองครั้งนี้เกิดขึ้นในคำสั่งแยกกันดังนั้นจึงไม่จำเป็นต้องร้องไห้ แต่มันคงไม่สำคัญที่จะเขียนมาโครแน่นอน
Stefan

1
สามารถเขียนแมโครนี้และรวมไว้ใน emacs ได้หรือไม่ ในขณะที่นักพัฒนาที่มีประสบการณ์มันไม่สำคัญสำหรับคนที่ต้องการยุบประวัติเลิกทำของพวกเขาและไม่รู้ว่าจะเริ่มที่ไหน - มันเป็นเวลาที่ยุ่งกับการออนไลน์และสะดุดในหัวข้อนี้ ... จากนั้นต้องค้นหาคำตอบที่ดีที่สุด - เมื่อพวกเขามีประสบการณ์ไม่เพียงพอที่จะบอกได้ ฉันได้เพิ่มคำตอบที่นี่: emacs.stackexchange.com/a/54412/2418
ideasman42

2

นี่คือwith-undo-collapseแมโครที่ใช้คุณลักษณะการเปลี่ยนแปลงกลุ่ม Emacs-26

นี่คือที่มีการเปลี่ยนแปลงหนึ่งบรรทัดเพิ่มatomic-change-groupundo-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.