eaf 和 evil 的磨合

eaf 不直接支持 evil,要想在eaf里面愉快的玩耍,可以让eaf buffer打开的时候 切换到 emacs或者 normal状态。

(add-to-list 'evil-insert-state-modes 'eaf-mode)

虽然每次打开 eaf-mode 的 buffer,会自动切到 insert state, 对于习惯 vim操作的人,经常会按Esc切换到normal state,这时候如果切换到其他 buffer, 再回到这个buffer的时候,需要按i切换到insert state,我经常在这里忘记切换到 insert state.

(add-to-list 'evil-emacs-state-modes 'eaf-mode)

缺点是如果习惯了leader的按键模式,你还需要设置M-leader键,也就是你需要适应两个 leader

改善:

(eval-after-load "evil"
  '(progn
     (defvar last-focus-buffer nil
       "Buffer currently in focus.")

     (defun buffer-focus-handler ()
       (interactive)
       (when (not (buffer-live-p last-focus-buffer))
         (setq last-focus-buffer nil))
       (when (and (eq (window-buffer (selected-window))
                      (current-buffer))
                  (not (eq last-focus-buffer (current-buffer))))
         (setq last-focus-buffer (current-buffer))
         (when (derived-mode-p 'eaf-mode)
           (evil-insert-state))))

     (add-hook 'buffer-list-update-hook #'buffer-focus-handler)))

当 eaf buffer获得焦点的时候,把 evil切换到 insert-state

6 个赞

可以把这段代码贡献到EAF中,然后给一个 evil 的选项,用户直接打开就可以兼容 evil 了。

感谢对EAF的支持。

EAF可以有一个contrib文件夹,专门用来接受用户提供的不适合直接加入eaf core的代码

这都是开放的,随时欢迎提交新补丁

最新版的EAF已经包括这个功能啊,谢谢补丁。

我是 evil 用户,但不用 space/doom emacs,没有一个专门的 leader-key

比如在 normal 下 ,我把 SPC 全局绑定为打开 buffer-list , 逗号是弹出一个最常用的功能的 hydra ,还有如 q,s 都有对应不同的全局函数。由于 eaf 的按键已经很接近 vim 了,所以对 eaf-mode 我是直接关闭 evil 的(这和进入 eaf 默认用 evil-emacs state 效果差不多,所以也不去 require eaf-evil),但我还是想在 eaf 下用 SPC 来切 buffer,用逗号弹出一个全局的 hydra 等等。 如果用

(eaf-bind-key comma-hydra/body "," eaf-browser-keybinding)

的设置方式,会使得在 text focus 状态下没法输入逗号。

我当前临时处理方案是写个函数判断是否处于 focus,如下

  (defun eaf-insert-or-comma ()
    (interactive)
    (if (eaf-call-sync "execute_function" eaf--buffer-id "is_focus")
        (eaf-py-proxy-insert_or_zoom_in)  ;; any insert_or_* command
      (comma-hydra/body)))
  (eaf-bind-key eaf-insert-or-comma "," eaf-browser-keybinding)

由于我有好几个这样的按键,每个都得这么绑定,重复代码比较多,另外执行到这些按键时每次执行都要给 python 端通信,确认是否 “is_focus", 导致在 text focus 下输入 SPC 逗号时有一点点延迟。

不知道你有没有这种需求,使得可以在 eaf 下更方便地对非控制键设置 emacs 内部的命令?

或者 @manateelazycat 能不能考虑在 emacs 这边同步一个类似 eaf–is_focus 的变量,不用每次和 python 通信?

尝试写一个补丁吧,实在看不懂再问我,先自己锻炼一下。

好的,感谢,有空尝试一下

今天看了 eaf 代码,参照着 caret 的部分,在 core/webengine.py 里的以下两个函数加了一个同步 focus 状态的操作,发现几行代码代码就行了,这个接口封装地真方便,python emacs 一体了 @manateelazycat :+1: :+1:

    @interactive(insert_or_do=True)
    def focus_input(self):
        ''' input in focus.'''
        if self.focus_input_js == None:
            self.focus_input_js = self.read_js_content("focus_input.js")

        self.eval_js(self.focus_input_js)
        eval_in_emacs('eaf--sync-text-focus-state', ["'t"])
       

    @interactive
    def clear_focus(self):
        ''' Clear the focus.'''
        if self.clear_focus_js == None:
            self.clear_focus_js = self.read_js_content("clear_focus.js")

        self.eval_js(self.clear_focus_js)
        eval_in_emacs('eaf--sync-text-focus-state', ["'nil"])

eaf–sync-text-focus-state 定义在 app/browser/eaf-browser.el 里

(defcustom eaf-browser-text-focus-state nil
  "Indicates whether text is being focus"
  :type 'boolean)

(defun eaf--sync-text-focus-state (text-focus-status)
  "Toggle text focus state given TEXT-FOCUS-STATUS."
  (setq eaf-browser-text-focus-state text-focus-status))

这样 emacs 这边就有一个判断是否 text-focus 状态的变量, 绑定按键用以下方式后,focus 时按逗号之类的就不卡了


  (defun eaf-insert-or-comma ()
    (interactive)
    (if eaf-browser-text-focus-state
        (eaf-py-proxy-insert_or_zoom_in)
      (comma-hydra/body)))
  (eaf-bind-key eaf-insert-or-comma "," eaf-browser-keybinding)

有了一个状态标识之后, eaf mode 下本身就相当于有 vim 的 normal 和 insert 状态,可以类似地加一些 hook或 advice 来判断状态变化,例如 参考 根据evil和内置输入法的状态调整modeline的背景色 里根据 eaf 是否 focus 来改变 modeline 颜色 output-2022-08-09-14:41:41

(以上 clear focus 显示有点问题,但主要展示 modeline 变化)

现在新的折腾需求是想把 text-input 内容也同步到某个变量或者隐藏的 buffer 里, 这样用 rime 或 pyim 的一些 predicate 功能就能统一,不用打开 edit buffer, 不知道可不可行。

另外,我用 doom-modeline,一般是用左下角那个小圆点来表示状态, @seagle0128 请教这个怎么根据某个变量值来切换颜色?

1 个赞

状态同步的代码发个补丁吧,我想办法融合到EAF里面去,感谢。

看这里

(defsubst doom-modeline--evil ()
  "The current evil state. Requires `evil-mode' to be enabled."
  (when (bound-and-true-p evil-local-mode)
    (doom-modeline--modal-icon
     (let ((tag (evil-state-property evil-state :tag t)))
       (if (stringp tag) tag (funcall tag)))
     (cond
      ((evil-normal-state-p) 'doom-modeline-evil-normal-state)
      ((evil-emacs-state-p) 'doom-modeline-evil-emacs-state)
      ((evil-insert-state-p) 'doom-modeline-evil-insert-state)
      ((evil-motion-state-p) 'doom-modeline-evil-motion-state)
      ((evil-visual-state-p) 'doom-modeline-evil-visual-state)
      ((evil-operator-state-p) 'doom-modeline-evil-operator-state)
      ((evil-replace-state-p) 'doom-modeline-evil-replace-state)
      (t 'doom-modeline-evil-normal-state))
     (evil-state-property evil-state :name t))))

好的,eaf 和 eaf-browser 我都发 PR 了

感谢答复,我先了解一下

我看了一下补丁, 两个补丁都是有问题的,正确方法如下:

同步更新浏览器的 focus 状态到 elisp 端

emacs-application-framework/webengine.py at 6ca09592cc269c36ce24f86710845fee0ffcf831 · emacs-eaf/emacs-application-framework · GitHub 这个函数里面, 添加 is_focus 判断, 然后调用 elisp 函数保存状态到 Emacs 端。

eventFilter 这个Qt函数的含义是, 任何事件都会通过这个函数过滤, 包括鼠标和键盘事件, 这样可以保证不漏任何事件, 你现在的补丁只是在调用 focus_input 或者 clear_focus 的时候才生效, 如果用户点击一下鼠标, 你现在的补丁就不会生效。

Elisp 同步 focus 状态

因为EAF支持浏览器多标签, eaf-browser-text-focus-state-p 需要用 defvar-local 来定义, 同时需要用 setq-local 来设置, 这样才能保证不同标签的 JavaScript focus 状态是隔离的,你现在的补丁会导致多个标签的 focus 状态相互影响。 为了保证区分不同标签的 focus 状态, python 端要传递 self.buffer_id 给 Elisp 函数, Elisp 函数接到 Python 消息后, 需要用 macro eaf-for-each-eaf-buffer 来找到 buffer-id 对应的 EAF buffer, 具体参考 emacs-application-framework/eaf.el at 6ca09592cc269c36ce24f86710845fee0ffcf831 · emacs-eaf/emacs-application-framework · GitHub 的实现。

你已经理解了 EAF Python<->Elisp 双向通信的原理, 但是逻辑还不够严密, 继续调整补丁吧, 加油!

我目前用 eaf-browser 主要还是在一个 buffer 下浏览页面,还在从 chrome+surfingkeys 慢慢转过来, 在 emacs 也几乎不用鼠标,确实都没想到这些问题 :sweat_smile:,感谢这么详细的说明,近期有空我再改一波。

See Synchronise input focus state to Elisp variable, handy for evil user. · emacs-eaf/emacs-application-framework@7f853ea · GitHub

你要的这个功能我已经完成了, 可以看看具体的实现, Elisp这边同步的变量是 eaf-buffer-input-focus , 只要检查 eaf-buffer-input-focus 就知道 EAF 浏览器是否处于 JavaScript 元素的输入状态了。

谢谢!我 pull 最新的提交后试了一下,发现eaf epc 很容易断掉,emacs -Q 还是有这问题:

deferred error : (error "Process EPC Server 2 <127.0.0.1:35400> not running")

怀疑是 event 过滤时调用 self.buffer.is_focus() 太频繁了(这个函数应该还得调用 js 代码?),大量按键、鼠标操作都会触发同步,测试时删掉这行确实就正常了。

感觉在键盘鼠标事件层面来过滤工作量比较大,大部分时候这些事件是不会触发 focus 状态变化的,不知道 focus 变化是否有一个统一的函数接口(类似 emacs 里打开文件的操作底层基本都调用 find-file),在这个接口加 hook来检查可能更好,事件只有真正触发该接口时再同步状态

qt层面肯定有focus状态变化的信号,但是网页内部的输入焦点只能靠js才能算出来,js一般就是检测input,textarea等标签的状态,没有固定的focus状态信号。

现在改成页面加载完或者用户手动调用按键命令的时候会更新。

更完备的需要 jump marker 或者按Tab键的时候也要一并处理一下。

这样就不会太消耗平常的性能。

我更新了代码,尝试发现在正常模式下用 i 进入文本输入框后,需要连续输入两个以上字符之后状态才会同步到 focus 状态,同时从文本输入框退出后也要先按一两个其他按键才能同步到 focus 状态。我不太清楚哪些会触发页面加载,但鼠标点击和滚动似乎是不会触发。

以我个人的体会,会在 emacs 里用 evil 操作浏览器,那基本上:

  1. 95% 场景下是键盘操作

    在前几层楼你提到说鼠标事件也应该同步前,我都没想到 eaf-browser 里还可以用鼠标,我之前也没用过(可能某些场景下无意识地滑过滚轮,但基本想不起来),你提到这个之后我最近测试代码才用上几次鼠标 :sweat_smile:

  2. 基本只按 i 进入 text focus, 按 esc 退出

    因此着重优化的就是 focus_input 和 clear_focus ,这两个操作不要去调用 self.is_focus(), 因为我看它还得调用 js 获得所有文本来检查,很多时候可能跟不上按键速度。 所以建议加上以下两句

    @interactive(insert_or_do=True)
    def focus_input(self):
        ''' input in focus.'''
        if self.focus_input_js == None:
            self.focus_input_js = self.read_js_content("focus_input.js")
    
        self.eval_js(self.focus_input_js)
        eval_in_emacs('eaf-update-focus-state', [self.buffer_id, "'t"])
    
    @interactive
    def clear_focus(self):
        ''' Clear the focus.'''
        if self.clear_focus_js == None:
            self.clear_focus_js = self.read_js_content("clear_focus.js")
    
        self.eval_js(self.clear_focus_js)
        eval_in_emacs('eaf-update-focus-state', [self.buffer_id, "'nil"])
    
  3. 剩下 5% 的情形

    这里面可能最重要的是鼠标点击,因为输入文本的时候一般就是鼠标点上去,然后输入,所以MouseButtonRelease 就可以(这种情况可以在按键失效,或者放松状态下单手操作之类的时候备用):

        if event.type() == QEvent.Type.MouseButtonRelease:
            eval_in_emacs("eaf-update-focus-state", [self.buffer_id, self.buffer.is_focus()]) 
    

    其他滑动滚轮、按键的事件都不太需要跟踪,因为还可以用类似前文讨论过的,加上一个状态标识,比如 modeline 颜色,文字等,可以根据标识来判断在哪个状态,如果状态不对,只是多按一个 esc 或者 i 的成本(但更多情况就是习惯性按 esc 了 :sweat_smile:)。实在要的话,加上一个两三秒间隔 timer 来同步也行。

以上配置个人用的还是比较顺畅。总结来就是:

  • buffer 之间状态隔离
  • 对最常用的 clear_focus, focus_input, 鼠标 release 进行状态同步,并且只有鼠标点击需要真正让 js 检查是否 focus

再次感谢大佬,我知道你不用 evil,但这么忙还来一起折腾这个特性,十分感谢并希望理解:pray:t2::pray:t2:

1 个赞