背景
Emacs 27 で tab-bar-mode
と tab-line-mode
が追加されました。機能紹介としては、 Emacs27で追加されるタブ機能について が GIF 付きで良かったです。
tab-bar-mode
は、ウィンドウの配置を記憶する『タブ』を提供するシンプルな機能です。外部パッケージの elscreen
, eyebrowse
, persp-mode
などは、同等以上の機能を提供するようですが、 tab-bar-mode
は組み込みなので、最初の導入には良いと思いました。
tab-bar-mode 導入後
centaur-tabs, neotree
と合わせて、次のような見た目になりました:
ターミナル上なので、 tmux の pane も使っています。
-
tab-bar-mode
はウィンドウの配置を記憶します。 -
centaur-tabs
はバッファをグルーピングしたタブバーを提供します。 -
neotree
はサイドバーです。
とうとう十分な多重化を手に入れました。
タブには、ブラウザやシェルを入れたら便利だと思います。
注意: Tab Bar Mode - Emacs Wiki によると、 tab-bar-mode
はバッファのグループを作ってくれるようですが、僕は centaur-tabs
を使っているため、 グループのことは考えません 。
メモ
-
tab-bar.el が
tab-bar-mode
の実装です。 -
C-x t
にショートカットが追加されます。 - macOS の Emacs では、
tab-bar-mode
の GUI が実装されていません。
設定
まず tab-bar-mode
を有効にします:
(tab-bar-mode 1)
ちなみに which-key
の設定は:
(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 → Smyx (Smyck の Emacs 版) の順でロードしています:
(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 にします:
;; 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 の Slack で @ROCKTAKEY さんが作成・共有してくださりました。ありがとうございました。 ROCKTAKEY さんはタブに文字列を表示する tab-bar-display も公開されています。
タブのデフォルト名
プロジェクト名にしました。名前を設定する関数は見つからず、 hook も無いため、 advice-add
を使いました:
;; 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)
タブを左右に動かす
左右端のタブは、反対側へループします:
(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) を検索したり、 awesome-tabs を検索したり、
describe-function
(の Counsel 版) から関数を探しました。counsel-M-x
だと非interactive
な関数が見つかりませんでした。
Evil
ここからは Evil に依存した設定となります。
ウィンドウまたはタブを閉じる
-
tab-bar-close-tab
は、最後のタブを閉じません。 -
evil-quit
は、最後のウィンドウを消去した場合、 タブが残っていても Emacs を閉じます 。
そこで、 Vim 準拠の evil-quit
を用意しました:
(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
好みでキーマッピングを設定します:
(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
を押したときに出るキーマップ表で、
t
を押せば、タブ操作一覧 (which-key
) がポップします:
general.el
を導入したら、 which-key
の表示も簡単に綺麗にできそうですね。
実際のコードは以下です。
Hydra
;; ------------------------------ 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)
)