🧑‍🏫

Neovim x null-ls x cspellの設定詳解

2022/07/26に公開

先日mason.nvimを導入しました。

https://zenn.dev/kawarimidoll/articles/367b78f7740e84

mason.nvimは前身のnvim-lsp-installerができなかったlinterのインストールにも対応しているため、これを機にcspellを使ってみようと思い立ちました。

https://cspell.org/

いい感じに動くところまで作れたので、設定を解説します。

cspellのインストール

mason.nvimを使います。

https://github.com/williamboman/mason.nvim

vim-plugなどのプラグインマネージャでmason.nvimを読み込んでください。

~/.config/nvim/init.vim
call plug#begin(stdpath('config') .. '/plugged')
Plug 'williamboman/mason.nvim'
call plug#end()

:MasonInstall cspellでcspellをインストールできます。
:Masonで開く設定画面のInstalledに表示されればOKです(この画面内からインストールすることもできます)。

:!cspell -Vで確認しても良いでしょう。

cspellをnull-lsに設定

cspellをインストールしただけではスペルチェックが動作しません。
null-lsを使ってcspellを実行し、バッファに反映させましょう。

https://github.com/jose-elias-alvarez/null-ls.nvim

~/.config/nvim/init.vim
call plug#begin(stdpath('config') .. '/plugged')
Plug 'williamboman/mason.nvim'
Plug 'jose-elias-alvarez/null-ls.nvim' " 追加
call plug#end()

" luaヒアドキュメントで設定を書くこともできますが、
" 設定が多くなる場合は、別ファイルに書いてluafileで呼び出すほうが
" 見通しが良くなると思います
luafile ~/.config/nvim/plugin_config/null_ls.lua
~/.config/nvim/plugin_config/null_ls.lua
local null_ls = require('null-ls')
local sources = {
  null_ls.builtins.diagnostics.cspell.with({
    diagnostics_postprocess = function(diagnostic)
      -- レベルをWARNに変更(デフォルトはERROR)
      diagnostic.severity = vim.diagnostic.severity["WARN"]
    end,
    condition = function()
      -- cspellが実行できるときのみ有効
      return vim.fn.executable('cspell') > 0
    end
  })
}

null_ls.setup({
  sources = sources
})

これで画面に表示が出るはずです。
なお、ここではwith()を使って、cspellのエラーレベルの変更と、コマンド実行確認を行っています。
詳細は https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/doc/BUILTIN_CONFIG.md を確認してください。

cspell設定ファイルの追加

cspellの標準設定では、一般的な英単語以外はすべて警告対象になってしまいます。
設定ファイルで辞書の追加などを行うことができます。

https://cspell.org/configuration/

~/.config/cspell/cspell.json
{
  "allowCompoundWords": true,
  "dictionaries": [
    "dotfiles",
    "lua",
    "user",
    "vim"
  ],
  "dictionaryDefinitions": [
    {
      "name": "dotfiles",
      "path": "~/.config/cspell/dotfiles.txt",
      "description": "dotfiles dictionary"
    },
    {
      "name": "user",
      "path": "~/.local/share/cspell/user.txt",
      "description": "User local dictionary"
    },
    {
      "name": "vim",
      "path": "~/.local/share/cspell/vim.txt.gz",
      "description": "Vim Script Language dictionary"
    }
  ],
  "languageSettings": [
    {
      "languageId": "lua",
      "locale": "*",
      "ignoreRegExpList": [
        "/require.*/"
      ],
      "dictionaries": [
        "lua"
      ]
    },
    {
      "languageId": "vim",
      "locale": "*",
      "ignoreRegExpList": [
        "/Plug .*/"
      ],
      "dictionaries": [
        "vim"
      ]
    }
  ]
}

null-lsでcspellを起動するときにこのファイルを読み込むよう設定します。

~/.config/nvim/plugin_config/null_ls.lua
-- 無関係の部分は省略
local sources = {
  null_ls.builtins.diagnostics.cspell.with({
    -- 起動時に設定ファイル読み込み
    extra_args = { '--config', '~/.config/cspell/cspell.json' }
  })
}

以下では、各設定項目について解説します。

allowCompoundWords

複合語を許容する設定です。

cspellはデフォルトでsnake_casecamelCaseを解釈するため、例えばdot_files dotFiles dotfilesを例に取ると以下のようになります。

  • dot_files/dotFilesdot+filesとみなされ、エラーが出ない
  • dotfilesは1単語とみなされ、エラーになる

このオプションをtrueに設定しておくと、dotfilesdot+filesと認識され、エラーが出なくなります。

dictionaries

追加辞書の名前の配列です。デフォルトの辞書にない語彙を追加することができます。

公式のリストはこちらです。

https://github.com/streetsidesoftware/cspell-dicts

各種プログラミング言語の辞書だけでなく、非英語の辞書や、companieslorem-ipsumなどの面白い語彙の辞書も提供されています。
city-names-finlandはなぜ公式で出しているのか謎…)

また、公式で提供されていない辞書も、次のdictionaryDefinitionsで設定することができます。

dictionaryDefinitions

ユーザー辞書の定義の配列です。
配列の要素は、namepathの要素をもつオブジェクトである必要があります。

  • nameは辞書の名前です。ここで定義した名前をdictionariesの配列にも登録しておく必要があります。
  • pathは辞書のパスです。相対パスを使うとcspell.jsonからの相対パスとして解釈されます。基本的には.txtファイルですが、.txt.gzファイルもそのまま使えました。

https://cspell.org/docs/dictionaries-custom/

例えば、Vimのキーワード辞書はcspell公式では提供されていないので、ここで設定する必要があります。
筆者はcoc-spellのリポジトリから拝借しました。

https://github.com/iamcco/coc-spell-checker/

また、筆者はdotfilesuserの2つのユーザー辞書を作成し、以下のように使い分けています。

  • dotfiles: dotfilesリポジトリに入れて公開し、他の環境でも使いたい語彙(kawarimidollなど)
  • user: スペルチェックからは除外したいが、公開はしたくない語彙(仕事で使う単語など)

null-lsの設定に、辞書を自動でダウンロード・作成する処理を追加しておくと便利です。

~/.config/nvim/plugin_config/null_ls.lua
-- vim辞書がなければダウンロード
if vim.fn.filereadable('~/.local/share/cspell/vim.txt.gz') ~= 1 then
  local vim_dictionary_url = 'https://github.com/iamcco/coc-spell-checker/raw/master/dicts/vim/vim.txt.gz'
  io.popen('curl -fsSLo ~/.local/share/cspell/vim.txt.gz --create-dirs ' .. vim_dictionary_url)
end

-- ユーザー辞書がなければ作成
if vim.fn.filereadable('~/.local/share/cspell/user.txt') ~= 1 then
  io.popen('mkdir -p ~/.local/share/cspell')
  io.popen('touch ~/.local/share/cspell/user.txt')
end

languageSettings

言語ごとにスペルチェックを無視する設定ができます。

https://cspell.org/configuration/language-settings/

筆者は、luaでは/require.*/、vimでは/Plug .*/が無視されるよう設定しています。
どちらもプラグイン名が後ろに続くことが想定され、特殊な語彙が含まれる可能性が高いためです。

設定ファイルまとめ

これまでの設定をまとめると、以下のようになります。

~/.config/nvim/init.vim
call plug#begin(stdpath('config') .. '/plugged')
Plug 'williamboman/mason.nvim'
Plug 'jose-elias-alvarez/null-ls.nvim'
call plug#end()

luafile ~/.config/nvim/plugin_config/null_ls.lua
~/.config/nvim/plugin_config/null_ls.lua
-- $XDG_CONFIG_HOME/cspell
local cspell_config_dir = '~/.config/cspell'
-- $XDG_DATA_HOME/cspell
local cspell_data_dir = '~/.local/share/cspell'
local cspell_files = {
  config = vim.call('expand', cspell_config_dir .. '/cspell.json'),
  dotfiles = vim.call('expand', cspell_config_dir .. '/dotfiles.txt'),
  vim = vim.call('expand', cspell_data_dir .. '/vim.txt.gz'),
  user = vim.call('expand', cspell_data_dir .. '/user.txt'),
}

-- vim辞書がなければダウンロード
if vim.fn.filereadable(cspell_files.vim) ~= 1 then
  local vim_dictionary_url = 'https://github.com/iamcco/coc-spell-checker/raw/master/dicts/vim/vim.txt.gz'
  io.popen('curl -fsSLo ' .. cspell_files.vim .. ' --create-dirs ' .. vim_dictionary_url)
end

-- ユーザー辞書がなければ作成
if vim.fn.filereadable(cspell_files.user) ~= 1 then
  io.popen('mkdir -p ' .. cspell_data_dir)
  io.popen('touch ' .. cspell_files.user)
end

local null_ls = require('null-ls')
local sources = {
  null_ls.builtins.diagnostics.cspell.with({
    diagnostics_postprocess = function(diagnostic)
      -- レベルをWARNに変更(デフォルトはERROR)
      diagnostic.severity = vim.diagnostic.severity["WARN"]
    end,
    condition = function()
      -- cspellが実行できるときのみ有効
      return vim.fn.executable('cspell') > 0
    end
    -- 起動時に設定ファイル読み込み
    extra_args = { '--config', cspell_files.config }
  })
}

null_ls.setup({
  sources = sources
})
~/.config/cspell/cspell.json
{
  "allowCompoundWords": true,
  "dictionaries": [
    "dotfiles",
    "lua",
    "user",
    "vim"
  ],
  "dictionaryDefinitions": [
    {
      "name": "dotfiles",
      "path": "~/.config/cspell/dotfiles.txt",
      "description": "dotfiles dictionary"
    },
    {
      "name": "user",
      "path": "~/.local/share/cspell/user.txt",
      "description": "User local dictionary"
    },
    {
      "name": "vim",
      "path": "~/.local/share/cspell/vim.txt.gz",
      "description": "Vim Script Language dictionary"
    }
  ],
  "languageSettings": [
    {
      "languageId": "lua",
      "locale": "*",
      "ignoreRegExpList": [
        "/require.*/"
      ],
      "dictionaries": [
        "lua"
      ]
    },
    {
      "languageId": "vim",
      "locale": "*",
      "ignoreRegExpList": [
        "/Plug .*/"
      ],
      "dictionaries": [
        "vim"
      ]
    }
  ]
}

ユーザー辞書への単語の追加

スペルチェックから除外したい単語があった場合、ユーザー辞書にその単語を追加すれば警告を消すことができます。
しかし、いちいち辞書ファイル(上記の設定でいうと~/.local/share/cspell/user.txt)を開いて単語をコピペするのは面倒です。
これについては別の記事で解説します。

https://zenn.dev/kawarimidoll/articles/2e99432d27eda3

謝辞

Vimのキーワード辞書に関しては@yutkatさんに教えていただきました。

https://twitter.com/yutkat/status/1551485506587750400

また、null-lsとcspellの設定も@yutkatさんのdotfilesをかなり参考にさせていただきました。

https://github.com/yutkat/dotfiles

Discussion