Emacs の tab-bar-mode を使ってみました

# 背景

Emacs 27 で `tab-bar-mode` と `tab-line-mode` が追加されました。機能紹介としては、 [Emacs27で追加されるタブ機能について](https://qiita.com/grugrut/items/1df4bdbe1453f4b06fc2) が GIF 付きで良かったです。

`tab-bar-mode` は、ウィンドウの配置を記憶する『タブ』を提供するシンプルな機能です。外部パッケージの `elscreen`, `eyebrowse`, `persp-mode` などは、同等以上の機能を提供するようですが、 `tab-bar-mode` は組み込みなので、最初の導入には良いと思いました。

## tab-bar-mode 導入後

[centaur-tabs](https://github.com/ema2159/centaur-tabs), [neotree
](https://github.com/jaypei/emacs-neotree) と合わせて、次のような見た目になりました:

![pic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594390/92bc94dd-ef93-9ca7-6ef4-71565b867904.png)

ターミナル上なので、 [tmux](https://github.com/tmux/tmux/wiki) の pane も使っています。

* `tab-bar-mode` はウィンドウの配置を記憶します。
* `centaur-tabs` はバッファをグルーピングしたタブバーを提供します。
* `neotree` はサイドバーです。

とうとう十分な多重化を手に入れました。
タブには、ブラウザやシェルを入れたら便利だと思います。

注意: [Tab Bar Mode - Emacs Wiki](https://www.emacswiki.org/emacs/TabBarMode) によると、 `tab-bar-mode` はバッファのグループを作ってくれるようですが、僕は `centaur-tabs` を使っているため、 **グループのことは考えません** 。

## メモ

* [tab-bar.el](https://github.com/emacs-mirror/emacs/blob/master/lisp/tab-bar.el) が `tab-bar-mode` の実装です。
* `C-x t` にショートカットが追加されます。
* macOS の Emacs では、 `tab-bar-mode` の GUI が実装されていません。

# 設定

まず `tab-bar-mode` を有効にします:

```elisp
(tab-bar-mode 1)
```

ちなみに `which-key` の設定は:

```elisp
(use-package which-key
    :init
    ;; a) show hints immediately
    (setq which-key-idle-delay 0.01
          which-key-idle-secondary-delay 0.01)
    ;; b) always press `C-h` to trigger which-key
    ;; (setq which-key-show-early-on-C-h t
    ;;       which-key-idle-delay 10000
    ;;       which-key-idle-secondary-delay 0.05)

    :config
    (define-key help-map (kbd "M") 'which-key-show-major-mode)
    (which-key-mode))
```

### 色

`tab-bar-mode` は最近の機能なので、古いテーマは、タブバーを着色してくれません。替わりに、先にサポート色の多いテーマをロードしてから、古いテーマで上書きすると良いと思います。

僕は [doom-themes](https://github.com/hlissner/emacs-doom-themes) →  [Smyx](https://github.com/tacit7/smyx) ([Smyck](http://color.smyck.org/) の Emacs 版) の順でロードしています:

```elisp
(use-package doom-themes
    :config
    (setq doom-themes-enable-bold t
          doom-themes-enable-italic t)
   (load-theme 'doom-opera t))

;; NOTE: `smyx` is NOT on Melpa. Download it and set up `load-path`.
(require 'smyx-theme)
(load-theme 'smyx t)
```

### Ivy で補完

`tab-bar-switch-to-tab` の補完を [Ivy](https://github.com/abo-abo/swiper) にします:

```elisp
;; use Ivy for `tab-bar-switch-to-tab`
(defun advice-completing-read-to-ivy (orig-func &rest args)
    (interactive
     (let* ((recent-tabs (mapcar (lambda (tab)
                                     (alist-get 'name tab))
                                 (tab-bar--tabs-recent))))
         (list (ivy-completing-read "Switch to tab by name (default recent): "
                                    recent-tabs nil nil nil nil recent-tabs))))
    (apply orig-func args))
(advice-add #'tab-bar-switch-to-tab :around #'advice-completing-read-to-ivy)
```

> この設定は [Emacs JP](https://emacs-jp.github.io/) の Slack で [@ROCKTAKEY](https://qiita.com/ROCKTAKEY/lgtms) さんが作成・共有してくださりました。ありがとうございました。 ROCKTAKEY さんはタブに文字列を表示する [tab-bar-display](https://github.com/ROCKTAKEY/tab-bar-display) も公開されています。

### タブのデフォルト名

プロジェクト名にしました。名前を設定する関数は見つからず、 hook も無いため、 `advice-add` を使いました:

```elisp
;; use project name as default tab name
(defun toy/set-tab-name-default ()
    (let ((proj-name (projectile-project-name)))
        (when proj-name (tab-bar-rename-tab proj-name))))

(advice-add 'tab-bar-new-tab :after (lambda (&rest x) (toy/set-tab-name-default)))
(add-hook 'window-setup-hook #'toy/set-tab-name-default)
```

### タブを左右に動かす

左右端のタブは、反対側へループします:

```elisp
(defun toy/tab-move-right ()
    (interactive)
    (let* ((ix (tab-bar--current-tab-index))
           (n-tabs (length (funcall tab-bar-tabs-function)))
           (next-ix (mod (+ ix 1) n-tabs)))
        ;; use 1-based index
        (tab-bar-move-tab-to (+ 1 next-ix))))

(defun toy/tab-move-left ()
    (interactive)
    (let* ((ix (tab-bar--current-tab-index))
           (n-tabs (length (funcall tab-bar-tabs-function)))
           (next-ix (mod (+ ix n-tabs -1) n-tabs)))
        ;; use 1-based index
        (tab-bar-move-tab-to (+ 1 next-ix))))
```

* `tab-bar-mode` のタブの index は 1 から始まります。
* 実は `(tab-bar--current-tab-index)` で現在のタブの index が分かります。
* `(funcall tab-bar-tabs-function)` はタブのリストを返します。

> この実装のための情報収集について
>
> ソース ([tabbar.el](https://github.com/emacs-mirror/emacs/blob/master/lisp/tab-bar.el)) を検索したり、 [awesome-tabs](https://github.com/manateelazycat/awesome-tab) を検索したり、 `describe-function` (の Counsel 版) から関数を探しました。 `counsel-M-x` だと非 `interactive` な関数が見つかりませんでした。

## Evil

ここからは [Evil](https://github.com/emacs-evil/evil) に依存した設定となります。

### ウィンドウまたはタブを閉じる

* `tab-bar-close-tab` は、最後のタブを閉じません。
* `evil-quit` は、最後のウィンドウを消去した場合、 **タブが残っていても Emacs を閉じます** 。

そこで、 Vim 準拠の `evil-quit` を用意しました:

```elisp
(defun toy/evil-save-and-quit()
    (interactive)
    (save-buffer)
    (toy/evil-quit))

(defun toy/evil-quit ()
    (interactive)
    (cond ((one-window-p) (toy/evil-quit-all))
          (t (evil-quit))))

(defun toy/evil-quit-all ()
    (interactive)
    (cond ((= 1 (length (funcall tab-bar-tabs-function))) (evil-quit-all))
          ;; last window, not last tab
          (t (tab-bar-close-tab))
          ))

;; [Evil] overwrite `evil-quit`
(evil-ex-define-cmd "q[uit]" 'toy/evil-quit)
;; NOTE: this is not perfect, e.g., when we press `C-w q`

;; [Evil] ovewrite `evil-quit-all` with safer one
(evil-ex-define-cmd "wq" 'toy/evil-save-and-quit)
(evil-ex-define-cmd "qa[ll]" 'toy/evil-quit-all)
```

ついでに `evil-quit-all` が Emacs を終了しない (タブを閉じる) ように変更しました。 Evil ユーザには `C-x C-c` があるので、 Emacs を終了するときはそちらを使います。

コマンドの上書きは完全ではなく、キーマッピングの上書きが出来ていませんが、ひとまず十分とします。

### Key mappings

好みでキーマッピングを設定します:

```elisp
(evil-define-key 'normal 'global
    ;; `tab-bar-mode`
    "[t" #'tab-bar-switch-to-prev-tab
    "]t" #'tab-bar-switch-to-next-tab
    "[T" #'toy/tab-move-left
    "]T" #'toy/tab-move-right

    ;; ついでに `centaur-tabs`
    "[b" #'centaur-tabs-backward
    "]b" #'centaur-tabs-forward
    "[g" #'centaur-tabs-backward-group
    "]g" #'centaur-tabs-forward-group
    "[{" #'centaur-tabs-move-current-tab-to-left
    "]}" #'centaur-tabs-move-current-tab-to-right

    ;; 他に `unimpaired.vim` 相当のキーマッピング、ウィンドウ移動のキーマッピングなど
    )
```

### Hydra

タブの操作には、 `C-x t` も使っています。

ウィンドウ操作の `hydra` に、タブ操作も追加しました。これが `SPC w` を押したときに出るキーマップ表で、

![hydra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594390/9c3d13d5-fe8a-f791-3350-26fcc726ddb0.png)

`t` を押せば、タブ操作一覧 (`which-key`) がポップします:

![hydra-t.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594390/8fc707ac-169e-c24e-affe-c8da79e689ca.png)

`general.el` を導入したら、 `which-key` の表示も簡単に綺麗にできそうですね。

実際のコードは以下です。

<details><summary>Hydra</summary><div>

```elisp
;; ------------------------------ Hydra ------------------------------

;; builtin!
(require 'winner)
(winner-mode 1)

(use-package windswap
    ;; https://github.com/amnn/windswap
    ;; windswap-left|right|up|down
    :commands (windswap-up windswap-down windswap-left windswap-right))

(defun toy/tab-move-right ()
    (interactive)
    (let* ((ix (tab-bar--current-tab-index))
           (n-tabs (length (funcall tab-bar-tabs-function)))
           (next-ix (mod (+ ix 1) n-tabs)))
        ;; use 1-based index
        (tab-bar-move-tab-to (+ 1 next-ix))))

(defun toy/tab-move-left ()
    (interactive)
    (let* ((ix (tab-bar--current-tab-index))
           (n-tabs (length (funcall tab-bar-tabs-function)))
           (next-ix (mod (+ ix n-tabs -1) n-tabs)))
        ;; use 1-based index
        (tab-bar-move-tab-to (+ 1 next-ix))))

(defvar toy/expand-unit 5)

(defhydra toy/hydra-window (:color red :hint nil)
    "
hjkl: focus     rR: rotate          t: tab (prefix)
HJKL: resize    =: equlize          C-h, C-l: tab focus
wasd: split     c/q: close          1-9: move to tab
WASD: swap      x: kill, X: both    u: undo window change
"

    ("u" winner-undo)
    ;; doesn't work
    ;; ("C-r" winner-redo)

    ;; tab-bar-mode (Emacs 27)
    ("C-h" #'tab-bar-switch-to-prev-tab)
    ("C-l" #'tab-bar-switch-to-next-tab)
    ("tr" #'toy/set-tab-name-default) ;; NOTE: defined in `ide.el`
    ("tR" #'tab-bar-rename-tab)
    ("tn" #'tab-bar-new-tab)
    ("tN" (lambda () (interactive)
              (tab-bar-new-tab)
              (call-interactively 'tab-bar-rename-tab)))
    ("tx" #'tab-bar-close-tab)

    ;; select tab
    ("t1" (lambda () (interactive) (tab-bar-select-tab 1)))
    ("t2" (lambda () (interactive) (tab-bar-select-tab 2)))
    ("t3" (lambda () (interactive) (tab-bar-select-tab 3)))
    ("t4" (lambda () (interactive) (tab-bar-select-tab 4)))
    ("t5" (lambda () (interactive) (tab-bar-select-tab 5)))
    ("t6" (lambda () (interactive) (tab-bar-select-tab 6)))
    ("t7" (lambda () (interactive) (tab-bar-select-tab 7)))
    ("t8" (lambda () (interactive) (tab-bar-select-tab 8)))
    ("t9" (lambda () (interactive) (tab-bar-select-tab 9)))

    ;; move tab
    ("tm1" (lambda () (interactive) (tab-bar-move-tab-to 1)))
    ("tm2" (lambda () (interactive) (tab-bar-move-tab-to 2)))
    ("tm3" (lambda () (interactive) (tab-bar-move-tab-to 3)))
    ("tm4" (lambda () (interactive) (tab-bar-move-tab-to 4)))
    ("tm5" (lambda () (interactive) (tab-bar-move-tab-to 5)))
    ("tm6" (lambda () (interactive) (tab-bar-move-tab-to 6)))
    ("tm7" (lambda () (interactive) (tab-bar-move-tab-to 7)))
    ("tm8" (lambda () (interactive) (tab-bar-move-tab-to 8)))
    ("tm9" (lambda () (interactive) (tab-bar-move-tab-to 9)))

    ;; select centaur-tabs tabs
    ("1" (lambda () (interactive) (tab-bar-switch-to-tab 1)))
    ("2" (lambda () (interactive) (tab-bar-switch-to-tab 2)))
    ("3" (lambda () (interactive) (tab-bar-switch-to-tab 3)))
    ("4" (lambda () (interactive) (tab-bar-switch-to-tab 4)))
    ("5" (lambda () (interactive) (tab-bar-switch-to-tab 5)))
    ("6" (lambda () (interactive) (tab-bar-switch-to-tab 6)))
    ("7" (lambda () (interactive) (tab-bar-switch-to-tab 7)))
    ("8" (lambda () (interactive) (tab-bar-switch-to-tab 8)))
    ("9" (lambda () (interactive) (tab-bar-switch-to-tab 9)))

    ;; focus
    ("h" #'evil-window-left)
    ("j" #'evil-window-down)
    ("k" #'evil-window-up)
    ("l" #'evil-window-right)

    ;; enlarge/shrink
    ("H" (lambda () (interactive) (enlarge-window-horizontally (- toy/expand-unit))))
    ("L" (lambda () (interactive) (enlarge-window-horizontally toy/expand-unit)))
    ("K" (lambda () (interactive) (enlarge-window (- toy/expand-unit))))
    ("J" (lambda () (interactive) (enlarge-window toy/expand-unit)))

    ;; split
    ("w" #'toy/sp-N)
    ("a" #'toy/sp-W)
    ("s" #'evil-window-split)
    ("d" #'evil-window-vsplit)

    ("W" (lambda () (interactive) (windswap-up)))
    ("A" (lambda () (interactive) (windswap-left)))
    ("S" (lambda () (interactive) (windswap-down)))
    ("D" (lambda () (interactive) (windswap-right)))

    ;; close
    ("c" #'toy/evil-quit) ;; NOTE: defined in `keymap.el`
    ("q" #'toy/evil-quit) ;; NOTE: defined in `keymap.el`

    ;; delete
    ("x" #'kill-this-buffer)
    ("X" #'evil-delete)

    ("C-s" #'save-buffer)

    ("r" #'evil-window-rotate-downwards)
    ("R" #'evil-window-rotate-upwards)
    ("=" #'balance-windows)

    ("z" #'toy/zen)
    ("b" 'evil-buffer-new)

    ("ESC" nil)
    )
```
</div></details>