🏃

Vim でアルファベット大文字の単語を楽に打つ裏技

2024/10/07に公開

はじめに

Vimmer が日々入力する文書やコードには、アルファベットの大文字が頻繁に登場します。大文字だけで構成された単語も珍しくありません。たとえば

  • "JSON" や "CAPTCHA" といった一部の技術用語
  • PYTHONPATHJAVA_HOME といった多くの環境変数
  • 多くの言語における定数リテラル

は大文字で構成されています。こういった単語をShiftキーで打つのは面倒ですし、手指に負担がかかります。小さな不便ですが、頻度を考えると案外バカにもできません。

こういった小さな不便の解消こそ、豊富な機能・カスタマイズ性を備える Vim の得意分野といえます。実際、Vim にはアルファベット大文字を入力するための様々なアプローチがあります。 たとえば gU オペレータを使えばノーマルモードで特定の範囲のアルファベットを大文字化できますし、 vim-capslock プラグインを使えば Vim 内で擬似的に Caps Lock 機能を使うことができます [1]

個人的には、さらにお勧めの方法があります。それは直前に入力した単語を大文字にするマッピングを挿入モードに生やすこと。以下のデモ動画を見ると具体的な動作イメージが湧きやすいでしょう。


紹介するマッピングで環境変数名を大文字にする様子

xdg_config_home という文字列の直後に <C-l> を押すことで、大文字の XDG_CONFIG_HOME に変換されています。便利ですね!

本記事では、このマッピングの導入方法、特徴、動作原理を詳しく説明します。

マッピングの導入方法

設定ファイルに以下をコピペするだけです。プラグイン等は不要です。

  • Vim script

    function s:toupper_prev_word()
      let col = getpos('.')[2]
      let substring = getline('.')[0:col-1]
      let word = matchstr(substring, '\v<(\k(<)@!)*$')
      return toupper(word)
    endfunction
    
    inoremap <expr> <C-l> "<C-w>" .. <SID>toupper_prev_word()
    
  • Lua(Neovim のみ)

    vim.keymap.set("i", "<C-l>",
        function()
            local line = vim.fn.getline(".")
            local col = vim.fn.getpos(".")[3]
            local substring = line:sub(1, col - 1)
            local result = vim.fn.matchstr(substring, [[\v<(\k(<)@!)*$]])
            return "<C-w>" .. result:upper()
        end,
        {expr = true}
    )
    

Vim script 版と Lua 版の2種類を書いてみました。どちらも効果は同じなので、自身の設定ファイルの形式に合わせて使ってください。 また、割当先に <C-l> を選んだのは完全に個人の好みです。導入するときはご自身の好きなキーに割り当ててください [2]

マッピングの特徴

このキーマップには以下の特徴があります。

  • 挿入モードから抜けることなく大文字に変換できる。
    • gU を用いる場合は一旦ノーマルモードに戻る必要がありますが、本マッピングなら挿入モードのまま大文字へと変換することが可能です。
  • 単語単位であれば1手で操作が完結するため楽。
    • 文章やプログラム中に SCREAMING_SNAKE_CASE な単語が散在するようなケースでは、切り替えの手間がない分 Caps Lock よりも楽です。
  • Undo やドットリピートを妨げない。
    • このマッピングを使用しても undo や編集操作が区切れることはなく、あたかも最初から大文字で打っていたかのように振る舞ってくれます。


テキスト挿入のドットリピートが問題なくできる様子

マッピングの動作原理

紹介するマッピングが動作する仕組みを理解するには、以下について知る必要があります。

  • <expr> マッピング
  • 挿入モードにおける <C-w>
  • 4つの Vim script の関数
  • Vim script における正規表現

以下、Vim script 版のコードをベースに説明します(Lua 版もやっていることはほぼ同じです)。

<expr> マッピング

マッピングを定義するコマンドの引数冒頭に <expr> を付けると、Vim script の式を利用してキーマップを定義できます。たとえば

nnoremap j gj

は「ノーマルモードで j が打たれたら、gj と打たれたときのように振る舞え」というマッピングですが、

nnoremap <expr> j v:count == 0 ? "gj" : "j"

は「ノーマルモードで j が打たれたら、まず v:count == 0 ? "gj" : "j" という式を評価せよ。そして、評価の結果得られた文字列が打たれたものと振る舞え」というマッピングとなります。 v:count == 0 ? "gj" : "j" は Vim script の3項演算子で構成される式であり、v:count == 0 が true なら "gj"、 false なら "j" という文字列に評価されます。 その結果、マッピングでのカウント指定がなければ gj、カウント指定があれば j が打たれたものとして振る舞うのです。

それを踏まえると、冒頭のマッピングは

inoremap <expr> <C-l> "<C-w>" .. <SID>toupper_prev_word()

とあります。これはつまり「<C-l> と打たれたら <C-w>s:toupper_prev_word() の評価結果が打たれたものと思え」という意味です [3][4]

挿入モードにおける <C-w>

挿入モードで <C-w> を押すと、カーソル直前にある単語を削除することができます。前述の説明と関数名を踏まえれば、だいたいやりたいことがわかってきたでしょうか。このマッピングは直前の単語を大文字に直接変換しているのではなく、「直前の単語を消して、大文字にして挿入し直す」という動作を行っているのです。

4つの Vim script の関数

ここでは以下が理解できていれば十分です。

意味 ヘルプ
getpos('.') カーソルの位置を取得する :h getpos()
getline('.') カーソルのある行の内容を取得する :h getline()
matchstr(s, re) 文字列 s の中身を正規表現 re で検索し、マッチしたら該当する文字列を返す :h matchstr()
toupper(s) 文字列 s のアルファベットをすべて大文字にする :h toupper()

これを踏まえると、この関数 s:toupper_prev_word() の動作は

" カーソル位置の column index を取得
let col = getpos('.')[2]
" カーソル行の内容を取得し、「行頭からカーソル位置まで」の部分文字列を得る
let substring = getline('.')[0:col-1]
" 部分文字列の末尾にある単語を取得
let word = matchstr(substring, '\v<(\k(<)@!)*$')
" 末尾の単語を大文字にする
return toupper(word)

となり、結果的に「カーソル直前の単語」を大文字にした文字列が返却されます。

Vim script における正規表現

ここでは以下の特殊文字を使っています。

パターン 意味 ヘルプ
\v Very magic モード。それ以降で特殊なパターンを用いる際に必要なエスケープ文字が最低限になる。 :h /\v
< 単語の境界にマッチする。 :h /\<
\k 'iskeyword'で指定されたキーワード文字にマッチする。 :h /\k
(re)@! いわゆる否定先読み。後続の要素が re にマッチしないときのみマッチする。 :h /\@!
(re)* re の0つ以上の繰り返しにマッチする。 :h /*
$ 文字列の末尾にマッチする。 :h /$

結果的に \v<(\k(<)@!)*$ という正規表現は、「'iskeyword'で構成されており単語区切りのない、文字列末尾の単語」を示すことになります。

なお、[a-zA-Z0-9_]*$ のような “雑な” 正規表現でもほとんどのケースで動くと思います(パフォーマンス面でも雑な方が良さそうです)。今回は 'iskeyword' オプションが特殊なケースでも動く汎用性の高い正規表現を書きましたが、より良い書き方があるかもしれません。好みで調整してください。

なお、 'iskeyword' に対応させたいなら \k*$ で良いのでは…と思われた方もいるかもしれませんが、実はこれだと不十分です。なぜなら「iskeyword のみで構成されているが間に単語区切りがある」ケースが存在するためです(例: 平成jump はすべて \k にマッチする文字で構成されていますが、平成jumpの間に単語区切りがあります)。否定先読みを使うようなややこしい表現になったのはこれが原因です。

おわりに

アルファベット大文字を楽に記述するためのマッピングを紹介しました。

実はこのマッピングには inoremap <C-l> <Esc>gUvbgi という元ネタがあります。こちらはワンライナーでシンプルに定義できるものの、ドットリピートが途切れてしまう課題がありました。それを解消するため、先日開催された Meguro.vim というイベントで隣の席の thinca さんからアドバイスをいただきつつ改善を重ねたのが、本記事で紹介したマッピングです。完成に至るまでに様々な紆余曲折がありました。そのあたりの経緯もいつか記事にしたいですね。

皆さんも Meguro.vim に参加して、Vim の使い心地をどんどん良くしていきましょう!

脚注
  1. Caps Lock 的な動作がほしければ、物理キーではなくプラグインを使うことをお勧めします。Caps Lock キーは Vim との相性が非常に悪いからです。ノーマルモードでうっかり Caps Lock をトグルすると、hjkl モーションすらままならなくなってしまいます...。 ↩︎

  2. 挿入モードのキーのため、基本的には <C-> 系の特殊文字に割り当てるのがよいでしょう。 ↩︎

  3. ちなみにここでの ".." は文字列結合の演算子です。 ↩︎

  4. <SID> は、マッピング中で s: 始まりの(スクリプトローカル)関数を使用するための特殊文字です。詳細な説明は :h <SID> 参照。 ↩︎

Discussion