经过 有没有办法在 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-mode
,toki-tabs-string
和 toki-tabs-update-hook
的文档给它配置一个 UI。
不使用 tab 的同学或许也可以试试 toki-tabs-switch-to-buffer-in-group
,它允许你切换到当前分组的 buffer,并且 buffer 是按使用频率排序的,相信也会很顺手。
8 个赞
其实我觉得可以给 Awesome-Tab 改进一个行为,按照频率来分组,而不是按照Mode来分组,这样越用越爽,反而常规的Mode/Project分组我觉得都不是很好用。
3 个赞
我刚注意到这个贴子,我发现我的想法和楼主是一模一样的。在最开始的时候也是打算放到 mode line 里面,是放三个。
在 buffer list 变化的时候更新,上下 tab 的时候不动。
我都实现完了才发现了这个帖子,有点可惜。。。。。
不过我最后觉得还是可以放在 tab-line-format 里面,然后用一个 face 把字体弄小一点。
QiangF
5
这个和mini-modeline整合一下一定很酷,增加buffer名称的最大长度限制,另外感觉当前buffer的名字弄成当前buffer的路径(如果有的话),不用重复显示,可以节省一些空间。
QiangF
6
弄了一个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))
QiangF
7
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)))
QiangF
9
字体随便弄的,theme是大师的modus-themes
QiangF
10
弄了一个配置,感觉还不错
(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)