一种按 buffer 使用频率排序的 tab 方案

经过 有没有办法在 emacs 实现 vscode 那样的 tab 分组方式? 中的讨论,我认识到 tab 的价值是「把常用的几个 buffer 显示出来,在它们之间快速切换」,这就要求一个统计 buffer 使用频率并排序的机制,但无论是 Emacs 自己还是现有的 tab 插件都并不提供这个机制。所以之前使用 awesome-tab 时,我都是手动把常用的 tab 扔到最前面的位置,但是要手动去做总是有点不爽。

现在我实现了一个能自动按照使用频率排序的 tab 方案。下面给出思路,参考实现在这里

使用频率统计

说到使用频率,最直接的体现应该是「在那个 buffer 中执行命令的次数」,这样的话只要定义一个 buffer-local 的变量来统计命令次数,在 post-command-hook 里面给它加 1 就好了。但是这样有两个问题:

  • 使用鼠标滚轮的话,动一下就会执行好多个 mwheel-scroll 命令,总觉得有点不公平。
  • 有时候打开一个 buffer 只是为了阅读,在里面执行的命令肯定没有编辑多,这也有点不公平。

实际上我们使用一个 buffer 的状态,不管是编辑还是阅读,总是敲一些命令,停一停,再敲一些命令。停下来的时候 Emacs 就会进入空闲状态。所以最终决定的方案是利用 run-with-idle-timer,空闲一秒钟之后给当前 buffer 的使用频率加 1。试用下来以后发现效果不错。

tab 展示

tab 展示的逻辑就是将 buffer 分组后,把当前分组中使用频率最高的 4 个 buffer 展示出来。如果要用的 buffer 在这 4 个里,就用「前一个/后一个 tab」命令来切换;不在这 4 个里,就用 switch-buffer 之类的命令去切换。也就是说,当前 buffer 是可以不被 tab 展示出来的,初看起来可能有点怪,实际上用下来感觉还挺合理的。

下一个问题就是我们需要在合适的时机,按使用频率更新 tab 的显示。我是在 buffer-list-update-hook 里,追踪 (buffer-list) 的变化来更新的,但在以下情况下不更新:

  • 活动的 buffer 是 minibuffer 时
  • 新增或删除的 buffer 是隐藏的 buffer(名字以空格开头)
  • 使用「前一个/后一个 tab」命令时

这样 tab 就不会在不合适的时候重新排序了。实际使用时感觉只有在用 switch-buffer 命令或切窗口的时候会更新。

效果

因为只展示四个 tab,占用的空间不会太长,所以我把它做到了 modeline 里面,省得在窗口上方再占一行了:

右边的 +2.. 表示当前分组还有两个 buffer 没有作为 tab 展示出来。我可以用一个命令把这些 buffer(也就是当前分组中不常用的 buffer)都杀掉。

假如当前 buffer 不在 tab 里面,也会把 buffer 名字在那个 +2.. 后面展示出来:

关于我的参考实现

由于在 modeline 上展示 tabs 必须把其他信息右对齐才会美观,考虑到大多数人的 modeline 配置都没有右对齐相关的技巧,这样就没法提供一个开箱即用的 UI,所以我就不打算把这个做成单独的包了。

想要体验的同学可以直接把这个文件拿下来,参考 toki-tabs-modetoki-tabs-stringtoki-tabs-update-hook 的文档给它配置一个 UI。

不使用 tab 的同学或许也可以试试 toki-tabs-switch-to-buffer-in-group,它允许你切换到当前分组的 buffer,并且 buffer 是按使用频率排序的,相信也会很顺手。

7 个赞

其实我觉得可以给 Awesome-Tab 改进一个行为,按照频率来分组,而不是按照Mode来分组,这样越用越爽,反而常规的Mode/Project分组我觉得都不是很好用。

3 个赞

完全同意!!

我刚注意到这个贴子,我发现我的想法和楼主是一模一样的。在最开始的时候也是打算放到 mode line 里面,是放三个。

在 buffer list 变化的时候更新,上下 tab 的时候不动。

我都实现完了才发现了这个帖子,有点可惜。。。。。

不过我最后觉得还是可以放在 tab-line-format 里面,然后用一个 face 把字体弄小一点。

这个和mini-modeline整合一下一定很酷,增加buffer名称的最大长度限制,另外感觉当前buffer的名字弄成当前buffer的路径(如果有的话),不用重复显示,可以节省一些空间。

弄了一个mini-modeline的配置

(luna-def-key
 "C-M-h" #'toki-tabs-previous
 "C-M-l" #'toki-tabs-next)

(luna-load-package toki-tabs
  :autoload-hook (mini-modeline-mode-hook . toki-tabs-mode)
  :config
  (defun roife/shorten-path (path &optional max-len)
    "Shorten PATH to MAX-LEN."
    (unless max-len (setq max-len 0))
    (if (and path (not (eq path "")))
        (let* ((components (split-string (abbreviate-file-name path) "/"))
               (len (+ (1- (length components))
                       (reduce '+ components :key 'length)))
               (str ""))
          (while (and (> len max-len)
                      (cdr components))
            (setq str (concat str (if (= 0 (length (car components)))
                                      "/"
                                      (string (elt (car components) 0) ?/)))
                  len (- len (1- (length (car components))))
                  components (cdr components)))
          (concat str (reduce (lambda (a b) (concat a "/" b)) components)))
        ""))

  (defun mini-modeline-buffer-dir ()
    (roife/shorten-path default-directory 30))

  (defvar my-modeline-background "black")
  (setq mini-modeline-r-format '("%e" mode-line-process
                                 (:eval (propertize (mini-modeline-buffer-dir)
                                         'face 'toki-modeline-path-face))
                                 " "
                                 mode-line-position
                                 mode-line-remote
                                 mode-line-mule-info
                                 mode-line-modified
                                 " "
                                 (:eval (awesome-tray-module-workspace-info)
                                  'face `((:background ,my-modeline-background)))
                                 " "
                                 (:eval (propertize (format-time-string "%H:%M")
                                         'face `((:foreground "green" :background ,my-modeline-background))))
                                 " "
                                 (:eval (propertize
                                         (awesome-tray-module-symon-info)
                                         'face `((:foreground "plum3" :background ,my-modeline-background))))))

  (defun toki-modeline-tabs ()
    "Return tabs."
    (if (bound-and-true-p toki-tabs-mode)
        (toki-tabs-string)
        ""))

  (setq mini-modeline-l-format '((:eval
                                  (if mini-modeline--msg
                                      (propertize mini-modeline--msg 'face `((:foreground "yellow")))
                                      (toki-modeline-tabs)))))

  (defun my-echo-tabs ()
    (let ((mini-modeline--msg nil))
      (mini-modeline-display 'force)))

  (add-hook 'toki-tabs-update-hook 'my-echo-tabs))

buffer name的长度限定,对中文支持不好

(defvar toki-buffer-name-half-length-max 10 "")

(defun toki-buffer-name (buf)
  (let* ((buf-name (buffer-name buf))
         (buf-name-length (length buf-name)))
    (if (> (/ buf-name-length 2) toki-buffer-name-half-length-max)
        (concat (substring buf-name 0 toki-buffer-name-half-length-max) ":"
                (substring buf-name (* -1 toki-buffer-name-half-length-max) buf-name-length))
    buf-name)))

请问截图中的字体是什么?看起来很清爽

字体随便弄的,theme是大师的modus-themes

弄了一个配置,感觉还不错

(luna-load-package toki-tabs
  :autoload-hook (mini-modeline-mode-hook . toki-tabs-mode)
  :config
  (setq toki-tabs-visible-buffer-limit 4)

  (defun roife/shorten-path (path &optional max-len)
    "Shorten PATH to MAX-LEN."
    (unless max-len (setq max-len 0))
    (if (and path (not (eq path "")))
        (let* ((components (split-string (abbreviate-file-name path) "/"))
               (len (+ (1- (length components))
                       (reduce '+ components :key 'length)))
               (str ""))
          (while (and (> len max-len)
                      (cdr components))
            (setq str (concat str (if (= 0 (length (car components)))
                                      "/"
                                      (string (elt (car components) 0) ?/)))
                  len (- len (1- (length (car components))))
                  components (cdr components)))
          (concat str (reduce (lambda (a b) (concat a "/" b)) components)))
        ""))

  (defun mini-modeline-buffer-dir ()
    (roife/shorten-path default-directory 30))

  (defface toki-modeline-path-face
      '((((background light))
         :foreground "#ff0000" :italic t)
        (t
         :foreground "#ff0000" :italic t))
    "Face for file path.")

  (defvar my-modeline-background "black")
  (setq mini-modeline-r-format '("%e" mode-line-process
                                 (:eval (propertize (mini-modeline-buffer-dir)
                                         'face 'toki-modeline-path-face))
                                 mode-line-position
                                 mode-line-remote
                                 mode-line-mule-info
                                 mode-line-modified
                                 (:eval (awesome-tray-module-workspace-info)
                                  'face `((:background ,my-modeline-background)))
                                 (:eval (propertize (format-time-string "%H:%M")
                                         'face `((:foreground "green" :background ,my-modeline-background))))
                                 (:eval (propertize
                                         (concat "B" (substring battery-mode-line-string 1 -4))
                                         ;; (awesome-tray-module-symon-info)
                                         'face `((:foreground "plum3" :background ,my-modeline-background))))))

  (defun toki-modeline-tabs ()
    "Return tabs."
    (if (bound-and-true-p toki-tabs-mode)
        (toki-tabs-string)
        ""))

  (setq mini-modeline-l-format '((:eval (toki-modeline-tabs))))

(defun my-echo-tabs ()
    (let ((mini-modeline--msg nil))
      (when (timerp mini-modeline--timer) (cancel-timer mini-modeline--timer))
      (mini-modeline-display 'force)
      (setq mini-modeline--timer
            (run-with-timer mini-modeline-echo-duration 0.3 #'mini-modeline-display))))

  (add-hook 'toki-tabs-update-hook 'my-echo-tabs))

(luna-def-key
 "C-M-h" #'toki-tabs-previous
 "C-M-l" #'toki-tabs-next)