VimでIDEのようにスペルチェックするプラグインを作った ~ spelunker.vim


『Vimでもスペルチェックしたい』 第2弾

単語のtypoを頑張らなくても無くせるようにスペルチェックを自動化したいという想いから、去年の今頃にCCSpellCheck.vimというVimが苦手としていたCamelCaseのスペルチェックにフォーカスしたプラグインを作ったのですが、今回は一般的なIDEのようにキャメルケース以外も対象としてチェックできるプラグインを作ってみました

今回作ったプラグインはこちらです。
https://github.com/kamykn/spelunker.vim

概要

  • 単語のチェックにspellbadword()を使っているので、Vimの機能であるspellの辞書、spellfileinternal-wordlistを利用しています
  • スペルチェックのタイミングはBufWinEnter,BufWritePostなので、基本的にはファイルを開いたタイミング、保存したタイミングです
    • → 前回同様、『待つタイミング』に処理をやらせてしまう方針
  • camelCaseだけではなく、snake_caseや小文字/大文字だけの単語も対応 (New!)
  • プログラミングに使われる単語のホワイトリストを用意(随時追加予定) (New!)
  • 言語別に関数名やキーワードなどのホワイトリストを持つことが可能 (New!)
    • → ただし今はVim(script)とPHPのみで、今後さらに追加予定
  • 接頭辞、接尾辞、複合語系と思われるワードは検出(または誤検出)はするけれども控えめのハイライトにしています (New!)
  • それでいて前作のCCSpellCheck.vimよりも少し処理速度が早い! (ハズ)

という感じです。
今後の言語別のホワイトリスト追加予定ですが、まずは自分の触ったことのある言語周りからを予定しています。JavaScript、Golang、Rust、Rubyと、あとそんなに触ってないけどPythonあたりですかね…!

↓ハイライトの様子

また、今回はtypoを修正する際に同じCaseの単語を一括置換するような機能も提供しています。

ZLコマンド

ZCコマンド

この機能については記事の後半で少し詳しく書きます。

spelunker.vimを新しく作った経緯

CCSpellCheck.vimを公開して1年経とうかという頃に、(思いもしない事だったのですが)海外の方からIssueが突如立てられるという事案が2回あり、そのうちの1件がcamel case以外も検出できたらいいなーというものでした。

https://github.com/kamykn/CCSpellCheck.vim/issues/2

考えてみればキャメルケースを正規表現で抜き出しているところを、スネークケースも含めるだけでイケる事に気づきました。もともとキャメルケースだけだったものから広く対象とする大きな変更もあるので、今回はコマンドなども整理しつつ別プラグインで作ることにしました。

2018年11月4日にこのIssueが立てられてからずっと作ってきたので、VimConf2018の直前に完成したという感じです

なんで『spelunker』という名前なの?

  • spellという単語が入ったような名前
  • プラグインが、バッファ内から探してきて見つけ出すようなイメージなため(探検的な)
  • 開発者(私)が現職がソシャゲ系のエンジニアなのでゲーム系の名前で探してみた(偉大なるタイトル名をお借りした)

あたりが決め手です

インストール

ようやくですが、ここからは使い方のほうに触れていきましょう。
NeoBundle、vim-plug ならインストールは下記の通りです。

" NeoBundleの人向け
NeoBundle 'kamykn/spelunker.vim'

" vim-plugの人向け
Plug 'kamykn/spelunker.vim'

セッティング

今回、内部的にはVimのspell機能を使っていますが、.vimrcなどでset spellが指定されている状態だと同じワードを検出してしまうことになりますので、nospellを指定して機能を切っていただきます。

set nospell

set nospellでも大丈夫な理由ですが、毎回保存時などのタイミングでspelunker.vimが処理する瞬間のみ内部でsetlocal spellされてから実行されるためです(終わったらsetlocalも元の設定に戻してます)。

オプション

デフォルト値で問題なければ何もしなくても大丈夫です。

" spelunker.vim の有効化 (1 / 0) (default 1)
let g:enable_spelunker_vim = 1

" スペルチェックの対象となる単語あたりの最小の文字数 (default 4)
" 例) 4という設定値の場合、lenとかxxxLen、xxx_lenとかのlenは3文字のため対象としません。
let g:spelunker_target_min_char_len = 4

" ZL/Zl コマンドで表示される候補数 (default 15)
let g:spelunker_max_suggest_words = 15

" 1バッファあたり検出する単語数の最大 (default 100)
let g:spelunker_max_hi_words_each_buf = 100

" highlightのグループ設定 (通常のtypo用)
let g:spelunker_spell_bad_group = 'SpelunkerSpellBad'

" highlightのグループ設定 (複合語等の場合用)
" Override highlight group name of complex or compound words. (default 'SpelunkerComplexOrCompoundWord')
let g:spelunker_complex_or_compound_word_group = 'SpelunkerComplexOrCompoundWord'

" Highlightの設定
highlight SpelunkerSpellBad cterm=underline ctermfg=247 gui=underline guifg=#9e9e9e
highlight SpelunkerComplexOrCompoundWord cterm=underline ctermfg=NONE gui=underline guifg=NONE

利用できるコマンド

ここで紹介するコマンドはすべてノーマルモードでカーソル元の単語に対して実行するコマンドになります。

ZL/Zlコマンド
Vimのspellsuggest()を利用した、修正候補のリストから選ぶタイプの修正コマンドです。
ZLはバッファ内のすべての同じCaseの単語を対象に書き換えますが、Zlはカーソル下の単語だけを修正します。

※ イメージ↓↓↓

ZC/Zcコマンド
Vimのcコマンドのときのような単語の置き換えを行います。
ZCはバッファ内のすべての同じCaseの単語を対象に書き換えますが、Zcはカーソル下の単語だけを修正します。

※ イメージ↓↓↓

Zg/ZG、 Zug/ZUG、 Zw/ZW、 Zuw/ZUW

Vimのコマンドであるzgなどの先頭のzを大文字にしたspeluncer.vim向けにカスタマイズされたコマンドです。
正しい単語に対してspellgood (Zg)でspellfileinternal-wordlistに登録することで、次から同じ単語が検出されなくなります。
internal-wordlistはVimを終了するまで保持されます。

" spellfileに対してのコマンド
" => zg (goodとして登録)
Zg

" => zw (wrongとして登録)
Zw

" => zug (good登録を取り消し)
Zug

" => zuw (wrong登録を取り消し)
Zuw

" internal-wordlistに対してのコマンド
" => zG (goodとして登録)
ZG

" => zW (wrongとして登録)
ZW

" => zuG (good登録を取り消し)
ZUG

" => zuW (wrong登録を取り消し)
ZUW

作った時の紆余曲折的なやつ

  • キャメルケース以外も対象にしたらハイライトする対象が結構増えて困った
    • ホワイトリスト絶対必要 → 共通しそうなやつから → 結局言語別に用意しようの流れ
      • (Vimのオプションとかfunction名が、spellsuggest()のような区切り文字がないようなものばかりなため、やるしか無いと思った)
    • spellfileに登録する単語がlowercaseだと、lowercaseしか正しい単語扱いにならない。
      • いつでもlowercaseでspellfile追加して、単語チェックに突っ込むのをlowercaseにすることを徹底した。
    • それでも、プログラミング界隈の単語(複合語系、接頭辞/接尾辞ついた系)がまだ少し誤検出があったため、それっぽいワードは控えめなハイライトにしておくなど考慮した
  • 対象とする文字列が増えて処理速度が重くなる件
    • ハイライトが多いと(=matchadd()しすぎると)、行移動で重くなる…? (1000件とか)
    • spellbadword()でチェックする対象の単語リストをuniqコマンド的な事をしてから、などの地道な最適化
  • Issueが日本からではなく英語でコミュニケーションとってくるような方から立てられたので、実は困っているのは日本人だけではなかった件について
    • Issueに返信するのに15分以上かかる英語力orz
    • でも、楽しい

まとめ

そんなこんなでこの開発には約3週間と意外と時間はかかってしまいましたが作りきったということで、2018年もVim活ができてよかったです…!
作ってみて思ったのですが一括変換は便利だなと思いつつ、これだけで別プラグイン化しても良さそうだなと思い始めてます。どうしようかな

何はともあれ、このプラグインが皆さんにとって良いものでありますように。
それでは皆さんtypoのない人生を