typos を使った Neovim でのスペルチェック
typo との戦い
僕もよくやってしまうんですが、人のコードをレビューしている時に typo をよく見かけます。
エディターの auto completion や最近流行の GitHub Copilot などは typo した状態で補完してくれたりするので、ミスした状態のコードが量産されていってしまいます。
どこかで一括置換すれば直せますが、修正漏れがあるとただのスペルミス修正なのに動かなくなってしまうなんてこともあり得るため、早めに直しておきたいものです。
VSCode のスペルチェック
typo で悩みたくないなーと感じて数年。
VSCode で Code Spell Checker という Extension を発見しました。
Code Spell Checker
この Extension から派生して今では CSpell として独立したものとして CLI でも動作するようになっています。
CSpell started out as an extension for VS Code: Code Spell Checker - Visual Studio Marketplace. When we started using VS Code, it did not have a spell checker. As a person that has trouble with spelling, I found this to be a great hinderance, thus the extension was born. At the suggestion of users, cspell was pulled out of the extension and into its own library and command line tools.
DeepLにて翻訳
CSpellはVS Codeの拡張機能としてスタートしました:Code Spell Checker - Visual Studio Marketplace。私たちがVS Codeを使い始めたとき、スペル・チェッカーがありませんでした。スペルチェックが苦手な私にとって、これは大きな障害でした。ユーザーの提案により、cspellは拡張機能から独立し、独自のライブラリーとコマンドラインツールになりました。
作者も VSCode さぞ辛かったことでしょう。
僕も愛用していて、すぐ typo に気付けるようになり、「あれ?なんでメソッドが動かないんだ?」みたいなことは減りました。
Neovim のスペルチェック
もっぱらここ2年くらいは Neovim を使用しています。
Neovim には公式で Spell Check に対応しています。
自分が使いこなせていないだけかもしれませんが、有効にしてみてもそこまで使い勝手がいいとは思えませんでした。
VSCode で使用していたような個別のスペルチェックプラグインは未だないようです。
スペルチェックを行う手段
では Neovim では簡易的なチェックしか出来ないのかってこともないようで。
何かしらのプラグイン経由でスペルチェック機能を搭載することはできます。
他にもあるかもしれませんが、手元で確認したのは次のプラグインです。
- none-ls.nvim(旧 null-ls.nvim)
- ALE
- nvim-lspconfig
CSpell にはどれも対応していたようでしたが、実際導入するためにはドキュメントがそこまであるわけでもなく VSCode のようにインストールしたらすぐ使えるという感じでもありません。
そもそも、これらのプラグイン導入自体のハードルが高いように感じます。
none-ls.nvim
CSpell を使うためにはじめ null-ls.nvim を利用していました。
null-ls.nvim は LSP に対応していないライブラリ等を lua を使って LSP として扱えるようにうまいことギャップをなくしてくれるライブラリです。
null-ls.nvim で builtin されているライブラリは豊富で CSpell も対応していました。
しかし、長らくメンテナンスされてきたライブラリでしたが作者は Neovim をプライマリエディタとして使わなくなっており、メンテナンスを維持することが困難になったためアーカイブすることが告知されてしまったのです。
今後どうなるかと思いきや、気付いたらコミュニティによって維持されることになり none-ls.nvim として再出発していました。
ただし、中身は null-ls.nvim のままであり作者の issue に書かれているように、問題を多く抱えたままです。
今後に期待ですが、別の実現方法を模索することにしました。
ALE
次にたどり着いたのは ALE です。
ALE は Neovim や Vim でテキストが変更されるとバッファの内容に対して linter を走らせエラーを返す仕組みで動いています。
また LSP Client として動作するため、定義ジャンプや Hover による情報表示等にも対応しています。
結論から言うとやりたいことの実現は出来たのですが、 null-ls.nvim より設定難易度はハードでした。
Neovim 用ライブラリではないため lua で書かれているわけでもありません。
ネットやら issue やらを dig って色々設定を見て回った結果です。
-- lazy.nvim での例
return {
'dense-analysis/ale',
config = function()
vim.g.ale_linters = {
['*'] = { 'cspell' },
}
vim.g.ale_cspell_options = '--config ~/.config/nvim/spell/cspell.json'
local function add_word_to_cspell()
local word = vim.fn.expand('<cword>')
local cspell_file = '~/.config/nvim/spell/cspell.json'
-- JSON ファイルを読み込む
local file, err = io.open(cspell_file, 'r')
if not file then
print('Failed to open ' .. cspell_file .. ': ' .. err)
return
end
local data = file:read('*a')
file:close()
-- JSON データを解析する
local status, json = pcall(vim.fn.json_decode, data)
if not status then
print('Failed to parse ' .. cspell_file .. ': ' .. json)
return
end
-- 単語を追加する
table.insert(json.words, word)
-- JSON ファイルを更新する
file, err = io.open(cspell_file, 'w')
if not file then
print('Failed to open ' .. cspell_file .. ': ' .. err)
return
end
file:write(vim.fn.json_encode(json))
file:close()
print('Added "' .. word .. '" to ' .. cspell_file)
-- ALE を再実行する
vim.cmd('ALELint')
end
vim.api.nvim_create_user_command('AddWordToCSpell', add_word_to_cspell, {})
end,
}
:AddWordToCSpell
コマンドを打つことで、カーソル配下の単語が cspell.json
へ保存され辞書登録されるようになっています。
正直、使い勝手がとても良いというわけでもなく、改善の余地はあります。
また、辞書にないものは当然警告が表示されまくります。
日本にしかない英単語なども全て警告が出てしまい、単語登録を毎回しなくてはならず、放置していると警告が鬱陶しいと感じるようになりました。
ここまで設定して使っていましたが CSpell である必要もないのではないか、もっと簡単に設定してできるものはないのかとも思い始めました。
要はスペルミスが見つかれば良いのです。
ただそれだけなのです。
ということで、また楽にできるものを探し始めることにしました。
nvim-lspconfig
そして、原点に返ってきました。我らが nvim-lspconfig。
Neovim で LSP を使うならば、避けては通れないくらいスタンダードなプラグインです。
様々な言語の LSP に対応しており、割と楽に(とはいえおまじない的な設定は必要ですが) LSP の設定が出来るプラグインです。
実はここにたどり着く前に none-ls.nvim の builtin されているプラグインで CSpell 以外に簡単に使えそうなものはないか物色していました。
builtin されているもの中でスペルチェックのものは見たところ次の通りです。
- spell
- codespell
- misspel
- textidote
- typos
この中で typos のみが nvim-lspconfig の server_configurations に含まれていました。
typos
typos は自然言語と言うよりソースコードに特化したスペルチェッカーのようです。
Rust 製であり、計測されてないので分かりませんが、十分な速度があると謳っています。
通常のスペルチェッカーとは思想が違い、誤検知を防ぐようにできているようです。
未知の単語を指摘するのではなく、既知のスペルミスのみを指摘することで、辞書に登録されてないものが大量にスペルミスとして出力されるといったことを減らしてくれます。
実際に CSpell と typos で比較してみると PayPay
のような辞書にはない単語は CSpell ではスペルチェックに検知されますが、 typos では検知されません。
更に recieve
という単語は正しくは receive
ですが、これは typos でも検知されます。
これによって辞書に載っていないような英単語を、都度単語登録する必要がなくなります。
とはいえ、検知してほしくない単語も少なからずあります。
それは辞書ファイルを作っておくことで除外することが可能です。
[default.extend-words]
recieve = "recieve"
typos_lsp を nvim-lspconfig に設定する
server_configurations を参考に設定に組み込んでいきます。
先ほどの .typos.toml
を読み込ませてあげるように設定してあげる必要があります。
設定は以下の通りです。
require('lspconfig').typos_lsp.setup({
init_options = {
config = '~/.config/nvim/spell/.typos.toml',
},
})
lsp.lua 全体の設定
mason.nvim や nvim-cmp などの設定も含まれています。
return {
'neovim/nvim-lspconfig',
dependencies = {
{ 'williamboman/mason.nvim', config = true },
'williamboman/mason-lspconfig.nvim',
'folke/neodev.nvim',
},
config = function()
local on_attach = function(_, bufnr)
local nmap = function(keys, func, desc)
if desc then
desc = 'LSP: ' .. desc
end
vim.keymap.set('n', keys, func, { buffer = bufnr, desc = desc })
end
nmap('gd', vim.lsp.buf.definition, '[G]oto [D]efinition')
nmap('gr', require('telescope.builtin').lsp_references, '[G]oto [R]eferences')
nmap('gI', vim.lsp.buf.implementation, '[G]oto [I]mplementation')
nmap('<leader>D', vim.lsp.buf.type_definition, 'Type [D]efinition')
nmap('K', vim.lsp.buf.hover, 'Hover Documentation')
nmap('<C-k>', vim.lsp.buf.signature_help, 'Signature Documentation')
nmap('gD', vim.lsp.buf.declaration, '[G]oto [D]eclaration')
nmap('<leader>wa', vim.lsp.buf.add_workspace_folder, '[W]orkspace [A]dd Folder')
nmap('<leader>wr', vim.lsp.buf.remove_workspace_folder, '[W]orkspace [R]emove Folder')
nmap('<leader>wl', function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, '[W]orkspace [L]ist Folders')
vim.api.nvim_buf_create_user_command(bufnr, 'Format', function(_)
vim.lsp.buf.format()
end, { desc = 'Format current buffer with LSP' })
end
require('neodev').setup()
local servers = {
lua_ls = {
Lua = {
workspace = { checkThirdParty = false },
telemetry = { enable = false },
},
},
}
-- nvim-cmp supports additional completion capabilities, so broadcast that to servers
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
-- Ensure the servers above are installed
local mason_lspconfig = require 'mason-lspconfig'
mason_lspconfig.setup {
ensure_installed = vim.tbl_keys(servers),
}
mason_lspconfig.setup_handlers {
function(server_name)
require('lspconfig')[server_name].setup {
capabilities = capabilities,
on_attach = on_attach,
settings = servers[server_name],
filetypes = (servers[server_name] or {}).filetypes,
}
end
}
require('lspconfig').typos_lsp.setup({
init_options = {
config = '~/.config/nvim/spell/.typos.toml',
},
})
end
}
設定方法はこれだけですが、個人的に今までで一番楽にスペルチェックを Neovim に組み込むことが出来ました。
まとめ
他にも簡単に設定できるものはあるのかもしれませんが typos は自分のやりたかったことに近いですし、何より単語登録を何回もする必要がない点は大きなメリットになり得ます。
まだ現在は単語登録するために .typos.toml
ファイルに直接書き込んでいますが、そのうちコマンドで出来るようにしたいと思います。
その時は結局 ALE と同じくらいのコード量になっていることでしょう。
最後に dotfiles を置いておきます。
まだまだ自分にとって最高のエディターに育てる道は果てしなく続きそうです。
Discussion