遇到奇怪的 recursive edit 错误

我的 EAF 里面有一个 eaf-monitor-key-event 函数, 用于监听 eaf buffer 的按键并转发给 python 进程。

实现方式是:

(add-hook 'pre-command-hook #'eaf-monitor-key-event)

(defun eaf-monitor-key-event ()
  (ignore-errors
    (with-current-buffer (buffer-name)
      (when (eq major-mode 'eaf-mode)
        (let* ((event last-command-event)
               (key (make-vector 1 event))
               (key-command (format "%s" (key-binding key)))
               (key-desc (key-description key))
               )

          ;; Uncomment for debug.
          ;; (message (format "!!!!! %s %s %s %s" event key key-command key-desc))

          (cond
           ;; Just send event when user insert single character.
           ;; Don't send event 'M' if user press Ctrl + M.
           ((and
             (or
              (equal key-command "self-insert-command")
              (equal key-command "completion-select-if-within-overlay"))
             (equal 1 (string-width (this-command-keys))))
            (eaf-call "send_key" buffer-id key-desc))
           ((string-match "^[CMSs]-.*" key-desc)
            (eaf-call "send_keystroke" buffer-id key-desc))
           ((or
             (equal key-command "nil")
             (equal key-desc "RET")
             (equal key-desc "DEL")
             (equal key-desc "TAB")
             (equal key-desc "<backtab>")
             (equal key-desc "<home>")
             (equal key-desc "<end>")
             (equal key-desc "<left>")
             (equal key-desc "<right>")
             (equal key-desc "<up>")
             (equal key-desc "<down>")
             (equal key-desc "<prior>")
             (equal key-desc "<next>")
             )
            (eaf-call "send_key" buffer-id key-desc)
            )
           (t
            (unless (or
                     (equal key-command "keyboard-quit")
                     (equal key-command "kill-this-buffer")
                     (equal key-command "eaf-open"))
              (ignore-errors (call-interactively (key-binding key))))
            )))
        ;; Set `last-command-event' with nil, emacs won't notify me buffer is ready-only,
        ;; because i insert nothing in buffer.
        (setq last-command-event nil)
        ))))

大部分情况都能很好的工作, 因为我转发完事件以后,会重置 last-command-event 为 nil, 这样 emacs 只会把事件转发给我, 但是在 eaf buffer 里面不论怎么按键都不会执行任何操作。

今天调试 Pdf Viewer 的时候, 当要跳转页面的时候, 需要在 Minibuffer 输入跳转的页数, 如果直接输入数字回车没有问题, 但是直接按 Ctrl + G 以后, 就会导致 Emacs 报下面的错误:

Error in pre-command-hook (eaf-monitor-key-event): (quit)
funcall-interactively: No recursive edit is in progress

这段错误的意思是, 在执行 pre-command-hook 一个钩子函数 eaf-monitor-key-event 的时候, eaf-monitor-key-event 内部发生了错误, 导致 emacs 就把 eaf-monitor-key-event 这个钩子函数从 pre-command-hook 的钩子列表中去掉, 导致 EAF 无法再通过 eaf-monitor-key-event 函数转发事件了。

我现在郁闷的是, 不知道 eaf-monitor-key-event 里面哪个函数触发了错误, toggle-debug-on-error 也无法调试 recursive edit 的错误。

发出来和大家讨论一下,说不定就找到解决问题的线索了。 :wink:

read–string 捕获 quit

我最开始也想在 read-string 那里防御一下, 怎么捕获 quit ?

我最开始是用了 unwind-protect

(defun eaf-input-message (buffer_id interactive_string callback_type)
  (let ((input-message ""))
    (unwind-protect
        (setq input-message (read-string interactive_string))
      (if (string= input-message "")
          (message "Got error")
        (eaf-call "handle_input_message" buffer_id callback_type input-message)))))

这个函数本身没有问题, 关键是 recursive-edit 的错误, 经过我的测试和 keboard-quit 中断 read-string 没有关系。

C-h f ignore-errors error -> quit

这个版本我也试过了:

(defun eaf-input-message (buffer_id interactive_string callback_type)
  (let ((input-message))
    (setq input-message (ignore-errors (read-string interactive_string)))
    (if input-message
        (eaf-call "handle_input_message" buffer_id callback_type input-message)
      (message "Got error")
      )))

问题的关键不在于 read-string 的防御, 没有用。 问题出在 pre-command-hook 的函数挂了。

想到一个非常贱的方法:

(defun eaf-monitor-key-event ()
  (unless
      (ignore-errors
        (with-current-buffer (buffer-name)
          (when (eq major-mode 'eaf-mode)
            (let* ((event last-command-event)
                   (key (make-vector 1 event))
                   (key-command (format "%s" (key-binding key)))
                   (key-desc (key-description key))
                   )

              ;; Uncomment for debug.
              ;; (message (format "!!!!! %s %s %s %s" event key key-command key-desc))

              (cond
               ;; Just send event when user insert single character.
               ;; Don't send event 'M' if user press Ctrl + M.
               ((and
                 (or
                  (equal key-command "self-insert-command")
                  (equal key-command "completion-select-if-within-overlay"))
                 (equal 1 (string-width (this-command-keys))))
                (eaf-call "send_key" buffer-id key-desc))
               ((string-match "^[CMSs]-.*" key-desc)
                (eaf-call "send_keystroke" buffer-id key-desc))
               ((or
                 (equal key-command "nil")
                 (equal key-desc "RET")
                 (equal key-desc "DEL")
                 (equal key-desc "TAB")
                 (equal key-desc "<backtab>")
                 (equal key-desc "<home>")
                 (equal key-desc "<end>")
                 (equal key-desc "<left>")
                 (equal key-desc "<right>")
                 (equal key-desc "<up>")
                 (equal key-desc "<down>")
                 (equal key-desc "<prior>")
                 (equal key-desc "<next>")
                 )
                (eaf-call "send_key" buffer-id key-desc)
                )
               (t
                (unless (or
                         (equal key-command "keyboard-quit")
                         (equal key-command "kill-this-buffer")
                         (equal key-command "eaf-open"))
                  (ignore-errors (call-interactively (key-binding key))))
                )))
            ;; Set `last-command-event' with nil, emacs won't notify me buffer is ready-only,
            ;; because i insert nothing in buffer.
            (setq last-command-event nil))
          ))
    (run-with-timer
     0.2
     nil
     (lambda ()
       (progn
         (add-hook 'pre-command-hook #'eaf-monitor-key-event))))))

先用 ignore-errors 报裹住 pre-command-hook 的函数内容, 一旦出了错误 ignore-errors 会返回 nil , 这个时候再在 ignore-errors 外面套上一个 unless , 一旦遇到错误, 就会被 unless 捕获, 然后再次用 run-with-timer 来执行 (add-hook 'pre-command-hook #'eaf-monitor-key-event)。

这样, 一旦在 pre-command-hook 中出现错误, 就再次添加函数到 pre-command-hook 中, 有一种逃不出我魔掌的感觉, 哈哈哈哈。

我解决方案还给我


(defun eaf-monitor-key-event ()
  (ignore-errors
    (with-current-buffer (buffer-name)
      (let* ((event last-command-event)
             (key (make-vector 1 event))
             (key-command (format "%s" (key-binding key)))
             (key-desc (key-description key))
             )
        (message (format "!!!!! %s %s %s %s" eveneo1t key key-command key-desc))
        (cond
         ((and
           (or
            (equal key-command "self-insert-command")
            (equal key-command "completion-select-if-within-overlay"))
           (equal 1 (string-width (this-command-keys))))
          (message "send_key %s" key-desc))
         ((string-match "^[CMSs]-.*" key-desc)
          (message "send_keystroke %s"  key-desc))
         ((equal "RET" key-desc) (condition-case nil (read-string "Hello:") (quit "")))
         (
          (or
           (equal key-command "nil")
           (equal key-desc "RET")
           (equal key-desc "DEL")
           (equal key-desc "TAB")
           (equal key-desc "<backtab>")
           (equal key-desc "<home>")
           (equal key-desc "<end>")
           (equal key-desc "<left>")
           (equal key-desc "<right>")
           (equal key-desc "<up>")
           (equal key-desc "<down>")
           (equal key-desc "<prior>")
           (equal key-desc "<next>")
           )
          (message "send_key %s" key-desc)
          )
         (t
          (unless (or
                   (equal key-command "keyboard-quit")
                   (equal key-command "kill-this-buffer")
                   (equal key-command "eaf-open"))
            (ignore-errors (call-interactively (key-binding key))))
          )))
      )))

(add-hook 'pre-command-hook #'eaf-monitor-key-event)

unwind-protect 不能捕获 quit

写个小函数测试一下就知道

为什么不看看 keyboard-quit 的源码

为什么不看看 ignore-errors 的实现

其实你还可以看看 C-h k C-h k 里是如何读取按键输入的

pre-command-hook 应该不是一个好方法

问题不是出在 read-string , 而是在 eaf-monitor-key-event 里面

我当然知道这些, 在问问题之前, ignore-errors, unwind-protect 我都试过了, 我当然也知道按键是怎么读取的, 我十多年前就开发了 one-key.el

只是我发现问题不是出在 read-string 这里, 我最开始也是怀疑的这里.

小伙子不要激动, 有话慢慢说, 慢慢讨论, 这里是大家互相学习的地方, 不要着急.

我的代码你运行了吗?

我把回车键绑定到了read-string 已经重现了你的问题

也 修复了他

运行了啊, 但是没有解决问题啊, 运行你的代码以后 eaf-monitor-key-event 完全就不工作了.

eaf-monitor-key-event 只是转发 eaf buffer 事件的, 而不是在 eaf-monitor-key-event 里面直接调用 read-string 读取用户输入的

所以, 你并没有理解清楚我描述的问题, 我在 minibuffer 里面 Ctrl + G 会影响 eaf-monitor-key-event , 但问题不在 read-string 这里.

我的代码贴错了 eveneo1t

哈哈哈, 如果有更好的方案, 还请指教

继续

;; This buffer is for text that is not saved, and for Lisp evaluation.
;; To create a file, visit it with C-x C-f and enter text in its buffer.

(defun eaf-monitor-key-event ()
  (message "DDD")
    (with-current-buffer (buffer-name)
      (let* ((event last-command-event)
             (key (make-vector 1 event))
             (key-command (format "%s" (key-binding key)))
             (key-desc (key-description key))
             )
        (message (format "!!!!! %s %s %s %s" event key key-command key-desc))
        (cond
         ((and
           (or
            (equal key-command "self-insert-command")
            (equal key-command "completion-select-if-within-overlay"))
           (equal 1 (string-width (this-command-keys))))
          (message "send_key %s" key-desc))
         ((string-match "^[CMSs]-.*" key-desc)
          (message "send_keystroke %s"  key-desc))
         ((equal "RET" key-desc) (condition-case nil (read-string "Hello:") (quit "")))
         (
          (or
           (equal key-command "nil")
           (equal key-desc "RET")
           (equal key-desc "DEL")
           (equal key-desc "TAB")
           (equal key-desc "<backtab>")
           (equal key-desc "<home>")
           (equal key-desc "<end>")
           (equal key-desc "<left>")
           (equal key-desc "<right>")
           (equal key-desc "<up>")
           (equal key-desc "<down>")
           (equal key-desc "<prior>")
           (equal key-desc "<next>")
           )
          (message "send_key %s" key-desc)
          )
         (t
          (unless (or
                   (equal key-command "keyboard-quit")
                   (equal key-command "kill-this-buffer")
                   (equal key-command "eaf-open"))
            (ignore-errors (call-interactively (key-binding key))))
          )))
      ))

(add-hook 'pre-command-hook #'eaf-monitor-key-event)

如果你真的运行了我的代码 会发现他根本就没有运行

所以你没有运行它