실행 취소 기록을 축소하는 방법?


17

음성 인식으로 Emacs를 제어 할 수있는 Emacs 모드에서 작업하고 있습니다. 내가 겪은 문제 중 하나는 Emacs가 실행 취소를 처리하는 방식이 음성으로 제어 할 때 예상대로 작동하지 않는다는 것입니다.

사용자가 여러 단어를 말한 다음 일시 중지하면이를 '발언'이라고합니다. 발화는 Emacs가 실행하는 여러 명령으로 구성 될 수 있습니다. 인식기가 발화 내에서 하나 이상의 명령을 잘못 인식하는 경우가 종종 있습니다. 그 시점에서 나는 "실행 취소"라고 말하고 이맥스 가 발언 내의 마지막 행동 만이 아니라 발언에 의해 수행 된 모든 행동을 취소하게하고 싶다 . 즉, 발언이 여러 명령으로 구성된 경우에도 이맥스가 발언을 실행 취소와 관련하여 단일 명령으로 취급하기를 원합니다. 또한 발언 전의 정확한 위치로 돌아가고 싶습니다. 정상적인 Emacs 실행 취소 가이 작업을 수행하지 않는 것을 알았습니다.

각 발언의 시작과 끝에서 콜백을 받도록 Emacs를 설정 했으므로 상황을 감지 할 수 있습니다. Emacs가 무엇을해야하는지 파악해야합니다. 이상적으로는 (undo-start-collapsing)다음 (undo-stop-collapsing)과 같이 호출 하고 그 사이에 수행 된 모든 작업은 마술처럼 하나의 레코드로 축소됩니다.

나는 문서를 통해 트롤링을하고 발견 undo-boundary했지만, 내가 원하는 것과 반대입니다. 발언 내의 모든 동작을 하나의 실행 취소 레코드로 축소하고 분할하지 않아야합니다. undo-boundary발화 사이 를 사용 하여 삽입이 개별적으로 간주되도록 할 수 있습니다 (기본적으로 이맥스는 연속적인 삽입 동작을 최대 한도까지 고려합니다).

다른 합병증 :

  • 내 음성 인식 데몬은 X11의 키 누름을 시뮬레이션하여 이맥스에 몇 가지 명령을 보내고 통해 일부를 전송 emacsclient -e말이 있다면, 그래서 (undo-collapse &rest ACTIONS)내가 포장 수있는 중앙 곳이 없습니다.
  • 나는 undo-tree이것이 더 복잡한 지 확실하지 않습니다. 이상적인 솔루션은 undo-treeEmacs의 정상적인 실행 취소 동작과 함께 작동하는 것 입니다.
  • 발언 내 명령 중 하나가 "실행 취소"또는 "재실행"이면 어떻게됩니까? 콜백 로직을 변경하여 항상 간단하게 유지하기 위해 별도의 발언으로 Emacs에 보낼 수 있다고 생각합니다. 그러면 키보드를 사용하는 것처럼 처리해야합니다.
  • 스트레치 목표 : 발화에는 현재 활성화 된 창 또는 버퍼를 전환하는 명령이 포함될 수 있습니다. 이 경우 각 버퍼에서 개별적으로 "실행 취소"라고 한 번하는 것이 좋습니다. 그렇게 멋질 필요는 없습니다. 그러나 단일 버퍼의 모든 명령은 여전히 ​​그룹화되어야하므로 "do-x do-y do-z 스위치 버퍼 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다음 마커를 찾을 때까지 반복해서 전화를 겁니다 . 그러나 나는 여기에 모든 종류의 합병증이 있다고 생각합니다. :)
glucas

경계를 제거하는 것이 더 나은 것 같습니다.
jch

실행 취소 트리를 사용하는 경우 버퍼 실행 취소 목록 조작이 작동합니까? 나는 그것이 undo-tree 소스에서 참조되는 것을 보았습니다. 그래서 나는 추측하지만 전체 모드를 이해하는 것이 큰 노력이 될 것입니다.
Joseph Garvin

@JosephGarvin 저는 연설로 Emacs를 제어하는데 관심이 있습니다. 사용 가능한 소스가 있습니까?
PythonNut

@PythonNut : 예 :) github.com/jgarvin/mandimus 포장이 불완전합니다 ... 코드는 부분적으로 내 joe-etc 저장소에 있습니다 : p 그러나 하루 종일 사용하고 작동합니다.
Joseph Garvin

답변:


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

@ 말라 바바 (Malabarba)는 항목 (apply identity nil)을 호출하면 아무 일도하지 않기 때문에 primitive-undo어떤 이유로 든 목록에 남아 있으면 아무 것도 중단하지 않습니다.
jch

추가 한 코드를 포함하도록 질문을 업데이트했습니다. 감사!
Joseph Garvin

(eq (cadr l) nil)대신 해야 할 이유가 (null (cadr l))있습니까?
ideasman42

귀하의 제안에 따라 @ ideasman42를 수정했습니다.
jch

3

실행 취소 기계에 일부 변경 사항은 "최근"일부 해킹 파산 viper-mode누를 때 : (호기심, 그것은 다음과 같은 경우에 사용되는 무너 이런 종류의 할 때 사용 된 ESC삽입 / 교체 / 버전을 완료, 바이퍼 전체를 축소하고 싶어를 단일 실행 취소 단계로 변경).

깔끔하게 수정하기 위해 새로운 기능을 도입했으며 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-collapseEmacs-26 변경 그룹 기능을 사용 하는 매크로입니다.

이것은 atomic-change-group한 줄 변경으로 추가 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.