16
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-10

背景

Emacs 27 で tab-bar-modetab-line-mode が追加されました。機能紹介としては、 Emacs27で追加されるタブ機能について が GIF 付きで良かったです。

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

tab-bar-mode 導入後

centaur-tabs, neotree
と合わせて、次のような見た目になりました:

pic.png

ターミナル上なので、 tmux の pane も使っています。

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

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

注意: Tab Bar Mode - Emacs Wiki によると、 tab-bar-mode はバッファのグループを作ってくれるようですが、僕は centaur-tabs を使っているため、 グループのことは考えません

メモ

  • tab-bar.eltab-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-themesSmyx (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 を押したときに出るキーマップ表で、

hydra.png

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

hydra-t.png

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)
    )
16
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?