分享下折腾emacs 29 的tab-line-mode经验

先说点题外话,emacs 提供了 previous-buffernext-buffer 两个命令 分别绑定在 C-x left C-x right 上, 在用tab 之前我一直是用这两个命令在最近访问过的buffer之间切换的。 并在这两个命令的基础上,对其做了简单封装,用于切换时,跳过 *Completions* *Compile-Log*" 这类比较烦人的的buffer。

这两天研究 tab-line-mode 的时候了 发现了switch-to-prev-buffer-skip 这个变量,它的值可以是一个函数,形如

(defun vmacs-switch-to-prev-buffer-skip(win buf bury-or-kill)
nil)
(setq switch-to-prev-buffer-skip #'vmacs-switch-to-prev-buffer-skip)

这个函数返回非nil值时,用next-buffer previous-buffer进行切换buffer时 便可将buf参数对应的buffer 跳过.

言归正传: 而我使用tab 的初衷有两个:

  1. 用tab 来将next-buffer/previous-buffer切换前后buffer可视化,用于对最近访问的三五个buffer之间进行切换
  2. *Completions* *Compile-Log*" 等buffer 与正常buffer分到不同的组
  3. 将vterm 分到同一个tab组里, 这个组里只显示vterm buffer,将能通过命令快速在相邻的tab页间进行切换

之前用了一段时间的 awesome-tab 和centaur-tab ,它们的分组功能可以满足我上面的第2、3条,但没法满足第1条 因为执行 next-buffer/previous-buffer 时,buffer的顺序是按 (buffer-list) 中的顺序来的, awesome-tab/centaur-tab 对buffer 进行分组,各组中buffer顺序无法与(buffer-list) 完全一致,所以无法满足第1个要求

所幸 awesome-tab/centaur-tab 都提供了 在相邻tab间进行切换的命令,与next-buffer/previous-buffer 行为类似, 于是上面第1条变成了 如何让最近访问过的buffer 对应的tab彼此相邻 , 为此也曾为awesome-tab/centaur-tab 提过issue , 如希望将新建的tab 放在当前tab 之前或之后,而不是放到整个列表的末尾。 还有就是当使用代码跳转或使用ivy/helm跳转时,希望跳转前后的两个buffer能挪到相邻的位置 centaur-tabs 有如下配置能实现诸如此类的功能。

(setq centaur-tabs-adjust-buffer-order 'left)
(centaur-tabs-enable-buffer-reordering)

emacs 自带了tab-bar-mode tab-line-mode 后 一直想研究下能否用它们实现类似的功能,这样又能少安装一个包了。

折腾了几天tab-line-mode后,发现基本能达到我的要求。

首先默认 tab-line-mode不进行分组 此时其tab的排序就是按(buffer-list) 的顺序排的 ,最近访问的buffer通常排在整个tab列表最后。 无论是通过代码跳转到一个buffer,还是使用consult-buffer/helm/ivy 切换到其他buffer,都会将你最近访问过的两个buffer 放到tab列表末尾,只有通过 previous-buffernext-buffertab-line-switch-to-prev-tab tab-line-switch-to-next-tab 进行的切换,才不会调整tab/buffer的顺序。 若是上面4条命令也会调整顺序,那你通过这4条命令只能在最末尾的两个buffer间来回切换。

要求1、2、3 可以认为将所有buffer 分成3组, 一组正常的buffer, 一组 为*Completions* *Compile-Log*" 等临时性的buffer 一组为vterm

但是一旦对tab 进行分组后 又会出现tab顺序无法保持与(buffer-list)顺序一致的情况。

不过我们可以通过对上面提到的 switch-to-prev-buffer-skip 进行定制后,来实现。 我们只需要保证 switch-to-prev-buffer-skip 判定为skipped 的buffer 恰好都不是当前分组内的buffer即可。 即:

  1. 若当前buffer 是正常buffer,则 switch-to-prev-buffer-skip 判断临时buffer或vterm为skipped ,
  2. 若当前buffer为临时buffer时,则将正常buffer或vterm 判断为skipped
  3. 若当前buffer为vterm ,则将 非vterm buffer判定为skipped

此时通过对 tab-line-tabs-window-buffers 加defadvice,来确保 tab-line-tabs-window-buffers 返回的tab列表与上面的判据一致 即可使用 previous-buffernext-buffertab-line-switch-to-prev-tab tab-line-switch-to-next-tab 切换前后相邻的tab.

代码如下:

(global-tab-line-mode t)
(global-set-key  (kbd "s-C-M-k") 'previous-buffer) ;H-k default C-x left
(global-set-key  (kbd "s-C-M-j") 'next-buffer)     ;H-j default C-x right
(setq tab-line-new-button-show nil)  ;; do not show add-new button
(setq tab-line-close-button-show nil)  ;; do not show close button
(setq tab-line-separator (propertize " ▶" 'face  '(foreground-color . "cyan")))

(setq switch-to-prev-buffer-skip #'vmacs-switch-to-prev-buffer-skip)
;; switch-to-prev-buffer 与 switch-to-next-buffer 时 skip 特定的buffers
;;而 tab-line-switch-to-prev/next-tab 恰好使用了上面两个函数
(defun vmacs-switch-to-prev-buffer-skip(win buf bury-or-kill)
  (when (member this-command '(next-buffer previous-buffer
                                           tab-line-switch-to-prev-tab
                                           tab-line-switch-to-next-tab))
    (cond
     ((vmacs-tab-vterm-p)                ;当前buffer是vterm
      (not (vmacs-tab-vterm-p buf)))     ;若buf 不是vterm,则skip
     ((vmacs-boring-buffer-p (current-buffer))
      (not (vmacs-boring-buffer-p buf)))
     (t                                 ;当前buffer是正常buffer
      (or (vmacs-boring-buffer-p buf)   ;若buf 是boring buf 或vterm,则跳过
          (vmacs-tab-vterm-p buf))))))

(defadvice tab-line-tabs-window-buffers (around skip-buffer activate)
  "Return a list of tabs that should be displayed in the tab line
but skip uninterested buffers."
  (let ((buffers ad-do-it))
    (cond
     ((vmacs-tab-vterm-p)               ;当前buffer是vterm
      ;; 只返回vterm buffer 作为当前tab group 的tab
      (setq ad-return-value (seq-filter #'vmacs-tab-vterm-p buffers)))
     ((vmacs-boring-buffer-p (current-buffer))
      (setq ad-return-value (seq-filter #'vmacs-boring-buffer-p buffers)))
     (t
      ;; skip boring buffer 及vterm
      (setq buffers (seq-remove #'vmacs-boring-buffer-p buffers))
      (setq ad-return-value  (seq-remove #'vmacs-tab-vterm-p buffers))))))

(defun vmacs-tab-vterm-p(&optional buf)
  (eq (buffer-local-value 'major-mode (or buf (current-buffer))) 'vterm-mode))

(defun vmacs-boring-buffer-p(&optional buf)
  (string-match-p (rx (or
                       "\*Async-native-compile-log\*"
                       "magit"
                       "\*company-documentation\*"
                       "\*eaf" "\*eldoc" "\*Launch " "*dap-"
                       "*EGLOT " "\*Flymake log\*"
                       "\*gopls::stderr\*" "\*gopls\*"
                       "\*Compile-Log\*" "*Backtrace*"
                       "*Package-Lint*" "\*sdcv\*" "\*tramp"
                       "\*lsp-log\*" "\*tramp" "\*Ibuffer\*"
                       "\*Help\*" "\*ccls" "\*vc"
                       "\*xref" "\*Warnings*" "\*Http"
                       "\*Async Shell Command\*"
                       "\*Shell Command Output\*"
                       "\*Calculator\*" "\*Calc "
                       "\*Flycheck error messages\*"
                       "\*Gofmt Errors\*"
                       "\*Ediff" "\*sdcv\*"
                       "\*Org PDF LaTex Output\*"
                       "\*Org Export"
                       "\*osx-dictionary\*" "\*Messages\*"
                       ))
                  (buffer-name buf)))

另附上我tab 相关配置链接

另附上只保留10个打开的文件的办法

;; 最多打开10个文件
(defun vmacs-prevent-open-too-much-files()
  (let* ((buffers (tab-line-tabs-window-buffers))
         (buffer-save-without-query t)
         (len (length buffers))
         (max 9) (i 0))
    (dolist (buffer buffers)
      (when (and (< i (- len max)) (>= len max))
        (when (buffer-live-p buffer)
          (with-current-buffer buffer
            (when (buffer-file-name buffer) (basic-save-buffer))
            (kill-buffer buffer))))
      (setq i (1+ i)))))
(add-hook 'find-file-hook #'vmacs-prevent-open-too-much-files)
9 个赞

用了一段时间,存在的问题是: 1 buffer列表的维护是基于window 维护的,即,window1、window2 单独维护各自的buffer list, 当把某个不常用的window 最大化后、某些buffer 会从tab消失。

所以我的改进思路是 维护一个main window,让常用buffer 都在main window中打开。 通过(window-at-x-y 20 20) 来判断哪个window 是main window . 然后常用的delete-other-windows 的绑定 C-x 1 我换成了vmacs-delete-window,其实现逻辑换成了 把当前buffer 在main window中打开后,关闭除了main window 之外的其他window,

delete-window 删除当前window,被我换成了 vmacs-delete-window ,也会判断当前window 是不是main window,如果是的话, 相当于把其他window 的buffer 拉到当前window 中,然后删掉那个其他

(vmacs-leader (kbd "1") 'vmacs-delete-other-windows) ;只保留当前窗口
(vmacs-leader (kbd "0") 'vmacs-delete-window)        ;删除当前窗口

(defun vmacs-kill-buffer-delete-window()
  (cl-letf (((symbol-function #'delete-window)
             #'vmacs-delete-window))
    (kill-buffer-and-window)))

(defvar vmacs--delete-window (symbol-function #'delete-window))
(defun vmacs-delete-window(&optional win)
  (interactive)
  (let ((main-win (window-at-x-y 20 20))
        (win (or win (selected-window)))
        )
    (if (eq main-win win)
        (progn
          (set-window-buffer main-win (window-buffer (next-window win)))
          (funcall vmacs--delete-window (next-window win)))
      (funcall vmacs--delete-window win))))

(defun vmacs-delete-other-windows()
  (interactive)
  (let ((main-win (window-at-x-y 20 20)))
    (if (eq main-win (get-buffer-window))
        (delete-other-windows)
      (set-window-buffer main-win (current-buffer))
      (delete-other-windows main-win))))

能用 gif 展示一下视觉效果吗

skip boring buffer抄了, 很好用, 感谢提供宝贵经验 :coffee:

tab-line-mode

感觉没什么可展示的,录了个简短的,在dired 中打开多个文件的效果,基本新打开的文件 都位于 整个tab的首位,后面的buffer 基本按照访问顺序展示。

其他代码跳转啊 等任何新打开文件的操作,都是把新打开的放首位

大佬, 可以体验 sort-tab , sort-tab 是按照频率排序的, 很好用。 :grinning:

我前阵子(1个月内) 试过想切换成sort-tab 不太满足我需求,我是期望有一定分组功能的, 比如, 我想把vterm 分到一个组内 ,像iterm2 等terminal 的多个tab页。 另外上面刚提到的痛点 暂时被我main window 的思路解决了,满足我个人的需求了。我基本上只开一个window ,偶尔开2个 也只是临时的,很快会关掉。基本够用了。

我用 sort-tab 的时候, 脑袋里面有时候也想到分组, 但是发现当标签很多的时候, 一旦分组后, 就会导致分组和其他未分组的标签切换效率的问题。

暂时没有想到两全其美的方法, 可以探讨下, 说不定自动排序 + 分组有更好的集成方案。

我现在只分3 个组, 一个是boring buffer 这个组我是不会主动去查看的,还有一个组vterm ,这个组 我使用vterm-toggle,用于打开最近的vterm(或关闭) 所以我的场景就是 想用vterm ,一键就过去了, 并且过去后 所有的vterm 都在tab中展示出来了,可以快速在几个vterm间切换, 不用的时候 也是一键隐藏了,立马回到 正常的第3个分组。

我也是倾向于 分组越少越好,我唯一想分出来的一组就是 vterm

sort-tab 默认是把那些隐藏buffer或者进程对应的buffer都默认不显示, 当然 switch-to-buffer 切换到隐藏buffer, sor-tab 也可以临时显示这些隐藏buffer的标签

sort-tab 对于正常的buffer全部都是按照使用频率排序

按照你的习惯, 我之前想过, 其实 sort-tab 可以配合 “浮动终端“ 就最好用。

什么是否浮动终端呢?

就是像 Popweb 那样, 用的时候把所有终端按照多标签窗口的方式来展示大概像下面这个样子

保持几个特性:

  1. 超级快, 我写的 GitHub - manateelazycat/deepin-terminal: Deepin Terminal written by vala 用的是 vala + vte 来实现的, 启动性能可以做到 50ms 就准备就绪了 (50ms 对于人来说就是瞬间)
  2. 弹出的窗口依然有多标签和分屏的功能
  3. 保持一定的透明度, 方便抄代码
  4. 一键弹出和隐藏
  5. 可以跑所有程序
  6. 和Emacs本身有广泛的互操作能力

但是平常我就Emacs和DeepinTerminal之间来回切换也挺方便的, 为啥要在Emacs外部用终端? 因为我经常开发Emacs插件, 把Emacs彻底搞废是每天都会遇到的事情, 所以外部终端保证Emacs挂了不要退出。

当然如果开发一个类似 popweb 特性的终端, Emacs退了它不退, Emacs启动它又贴过来就很方便了。


不成熟的想法, 就当抛砖引玉了, 主要是浮动终端对我效率提升不大, 重新写一个像深度终端的东西, 要打磨好细节还挺费劲的, 动力不足。

2 个赞

我是直接展示buffer-list,最新的就排在最前面,只需要写个tab-line-tabs-function就行了,在自定义函数里过滤buffer name,如:

(setq tab-line-tabs-function 'ep-tabbar-buffer-list)
(defun ep-tabbar-buffer-list ()
  (delq nil
        (mapcar #'(lambda (b)
                    (cond
                     ;; Always include the current buffer.
                     ((eq (current-buffer) b) b)
		     ((string-match "^TAGS\\(<.*>\\)?$" (format "%s" (buffer-name b))) nil)
                     ;;((string-match "^magit.*:.*" (format "%s" (buffer-name b))) nil)
                     ((string-match "^magit-.*:.*" (format "%s" (buffer-name b))) nil);; 排除magit-process
                     ((buffer-file-name b) b)
                     ((string-match "^\*gud-.*" (format "%s" (buffer-name b))) b) ;; gud buffer
                     ((string-match "^\*Embark .*" (format "%s" (buffer-name b))) b)
		     ((member (buffer-name b) EmacsPortable-included-buffers) b)
                     ((char-equal ?\  (aref (buffer-name b) 0)) nil)
                     ((char-equal ?* (aref (buffer-name b) 0)) nil)
                     ((char-equal ?: (aref (buffer-name b) 0)) nil) ;; 排除dired-sidebar buffer
                     ((buffer-live-p b) b)))
                (buffer-list))))

从楼主拿走tab-line-separator了,很好看,之前以前是close button隔开。

另外我是用的pop-select 跟vs等一样用CTRL+TAB来切换buffer的,在来回切换最近两个buffer时非常快。

2 个赞

这里的意思我可以这样理解吗? 如果一开始就按照 projectile 或者 term 模式等进行分组, 那么当需要频繁切换两个不同 projectile 的 buffer 时候就会很麻烦.

我认为上面的问题, 是因为根据 project / mode 等预先设定了分组, 缺乏灵活性. 如果分组是手动设定, 而且可以手动解除, 就没有这个问题了.

分组的方法我有个想法, 给 buffer 设定字母 i,j,k,l. 同一个字母的 buffer 自动合为一组.

没有设定为分组的照旧, 设定为分组的就由同一组中最高优先级的 buffer 来做为排序, 它的组员不管优先级如何都依附聚合它后面. 那么取消也容易, 只要把某个 buffer 取消分组, 该 buffer 就恢复正常.

| server.py | client.py | …| … | term ./sever.py | … | term ./client.py |

| (K) server.py | (K) term ./server.py | (J) client.py | (J) term ./client.py | … | … | …|

上面所示, 未分组之前, 临时打开的终端优先级最低, 排到后面去了, 但是通过让它和 server.py 分一组, 它就可以临时共享 server.py 的优先级.

一点想法, 希望对您有帮助. :grinning:

实践证明,标签规则不能太复杂了,心累。

哇塞,原来deepin的终端是懒猫大佬写的呀!

vala那版本是我写的,新版的deepin terminal功能太弱了

先膜拜一下大佬们的各种华山论剑,不知道理解没有大佬们的意识流

不太清楚buffer tab可以做类似swiper的二级过滤搜索跳转么?或者直接类似swiper一下不过类似tab页内文本搜索跳转?那样分多少组都可以直接到想去的地方?

还是大佬们讨论的已经支持那样了?

:slightly_smiling_face:

(defun my/switch-prev-tab ()
  (interactive)

  (let* ((tabs (tab-line-tabs-window-buffers))
        (n (seq-position tabs (current-buffer)))
        (l (length tabs)))

    (if (> n 0)
          (tab-line-select-tab-buffer (elt tabs (- n 1)))
      (tab-line-select-tab-buffer (elt tabs (- l 1))))
    ))


(defun my/switch-next-tab ()
  (interactive)

  (let* ((tabs (tab-line-tabs-window-buffers))
         (n (seq-position tabs (current-buffer)))
         (l (length tabs)))

    (if (< n (- l 1))
        (tab-line-select-tab-buffer (elt tabs (1+ n)))
      (tab-line-select-tab-buffer (elt tabs 0)))
    ))

我是这样做的

1 个赞