掲題の話は、Googleで検索すると、いくつか記事がヒットします。
ただ、恐らく記事を書いた方に macOS/linux 利用者が多いのか、Windowsでは色々うまくいかなかったので、その記録です。
この記事は 2024/02/29 うるう年の肉の日の夜に記載しております。今日は肉を食べませんでしたけど。
なお、私自身は Vimmerと言えるほどneovim等に詳しくありません。
何か気が付いた事、こうやればもっと楽に出来るよ等あればご教示いただけますと幸いです。
事前告知
- 空白を含むパスのファイルを編集する場合、efm-langserverから textlintをかける事は対応が難しいです
- ネット上では textlintで mason使わない方が良いのでは的な意見も散見されますが、masonで進めています
- 私の環境では以下でちゃんと動いていますが、バージョンが変わるとダメになる可能性があります
- SHIFT-JIS/DOS改行のテキストファイルについては未確認です
環境
ベース
- Windows 11 Pro 22H2
- neovim v0.9.5
- node v20.11.1
- jq 1.7.1
nvim周り
- mason.nvim (mason/mason-lspconfig)
- nvim-lspconfig
- efm-langserver
- textlint
導入
neovim, node.jsの導入、neovim pluginの mason、nvim-lspconfigの導入等は省略します。
Googleで検索すればたくさん出てきますので😅。
jqの導入
textlintの出力は、json形式に構造化することでプログラム的に扱いやすくなります。
これを加工し、efm-langserverに渡してnvimの画面に反映させます。
Windows 11だと winget install jqlang.jq
で入ります。
私は chocolateyも併用しているので choco install jq
で入れました。
nvimの :terminal
から jq が起動出来る事を確認しておきましょう。
textlintとefmの導入
textlintと efm-langserver(efm)を masonから導入します。
ここも :Mason
からインストールするだけなので詳細な手順は省略しますが、以下を読んで実際に動作するようになっている事を確認しておきましょう。
masonでの導入による副作用と確認
masonでlanguage serverを導入することで以下のような状況になります。
- nvim環境の中では PATHとして
%LOCALAPPDATA%\nvim-data\mason\bin
がパスの先頭に設定される - 導入した Language Serverを起動する
.CMD
ファイルが、このパスに作成される
ここで重要なのが、masonによりPATHの先頭に設定されることです。
npm install -g
等で別途textlintを導入していたとしても、nvimの中では masonで導入したtextlintが優先されます。
textlintはコマンドとして導入されている方も多いかと思います。その場合には masonでの導入は重ねて行う必要は無いでしょう。
いずれにせよ、事前確認として nvimの :terminal
でPATHを確認できます。また、その中で textlint
や efm-langserver
等のコマンドが叩けることを確認しておきましょう。
mason-textlint-helper.luaの導入
これは私が作成したモジュールです。こちらにあります。
textlintへのモジュールの追加や、.textlintrc.json 等の取り扱いが面倒だったので作成しました。
cloneするとかcopyするとかして、nvimの設定フォルダのluaフォルダ配下に置いてください。
jqのfilterファイルを作っておく
Windowsのコマンドラインは少し呪われています。これに jq のfilter呪文を理解させる事は困難が伴います。無理をすると、うまく動かずSAN1値を減少させる危険があります。
このため、コマンドラインから指定せず、filterファイルを使って指定します。
nvimの設定ファイル中から導出することが簡単な、nvimの構成ファイルディレクトリに保存する事とします。
nvim "%LOCALAPPDATA%\nvim\textlint.jq"
等として、以下の内容の textlint.jq
を作ります。
.[] | .filePath as $filePath | .messages[] | "1;\($filePath):\(.line):\(.column):\n2;\(.message | split("\n")[0])\n3;[\(.ruleId)]"
なお、jqの filterは、こちらで紹介されていたものを利用させていただいております🙇。
mason-lspconfigの setup-handler の追加
mason-lspconfigの setup handler にefmを追加します。
初期化時に 先ほど作った textlint.jq へのパス等を導出しておきます。
その後 lspconfig で efmをセットアップします。
local path_separator = package.config:sub(1, 1)
local config_path = vim.fn.stdpath("config")
local is_windows = vim.fn.has('win32') == 1
local normalize_path_separator = is_windows and function(path)
return path:gsub('/', '\\')
end or function(path)
return path:gsub('\\', '/')
end
require("mason-lspconfig").setup_handlers {
--[[ .... snip .... ]]
-- efm for textlint
["efm"] = function()
-- The .textlintrc file is searched in the following locations:
--
-- * The directory of current buffer file when this function is called.
-- * Traverses up through the parent directories of it.
-- * If not found, it looks '.textlintrc.json' in the configuration directory of nvim.
--
-- If you place the .textlintrc file in a folder with .git, set useRootMarkers to true.
local useRootMarkers = false
-- using textlint command under mason packages.
local textlint_bin = "textlint"
local config_path = vim.fn.stdpath("config")
local textlint_config = ''
if not useRootMarkers then
local textlint_helper = require('mason-textlint-helper')
local textlint_config_path = textlint_helper.SearchTextLintRC()
if textlint_config_path == '' then
textlint_config_path = config_path .. path_separator .. '.textlintrc.json'
end
if vim.fn.filereadable(textlint_config_path) == 1 then
textlint_config_path = normalize_path_separator(textlint_config_path)
textlint_config = " -c " .. textlint_config_path
-- set textlint config to textlint_helper.lua
textlint_helper.TextLintLSConfig = textlint_config_path
end
end
local textlint_cmd = textlint_bin .. textlint_config .. " -f json ${INPUT}"
-- jq filter for textlint output
local textlint_jq_filter = config_path .. path_separator .. 'textlint.jq'
local jq_cmd = "jq -r -f " .. normalize_path_separator(textlint_jq_filter)
local textlint = {
-- https://github.com/mattn/efm-langserver/blob/master/schema.md
lintCommand = textlint_cmd .. " | " .. jq_cmd,
lintFormats = { '%E1;%f:%l:%c:', '%C2;%m', '%C3;%m%Z' },
lintIgnoreExitCode = true,
lintOnSave = true,
}
require("lspconfig").efm.setup {
settings = {
rootMarkers = {
".git/",
},
languages = {
markdown = { textlint },
text = { textlint },
}
},
filetypes = {
"markdown",
"text",
},
single_file_support = true,
}
end
以下に、ここでの注意点を書いておきます。
lintCommand
正常に動作する例:
lintCommand = textlint_bin .. " -f json ${INPUT} | jq -r -f " .. textlint_jq_filter
このlintCommandですが、実際にコマンドラインに投入されるタイミングで、ダブルクォートがエスケープされてコマンドに投入されます。
このエスケープをCMD.EXEが理解できず、そのままtextlintコマンドに渡されてしまいます。
以下の様にすると 対象ファイルを読み込む事が出来ずに textlintが動きません。
エラーになる例:
lintCommand = textlint_bin .. ' -f json "${INPUT}" | jq -r -f ' .. textlint_jq_filter
${INPUT}
をダブルクォートで挟んでいるかどうかの違いです。
この違いは 編集中のファイルのパスが半角スペースを含むときに関係します。
ダブルクォートが無い場合、${INPUT}
が複数のパラメータとしてパースされ、正しくファイルが読み込まれなくなります。
同様の事から textlint_bin
や textlint_jq_fileter
で指定しているパスもダブルクォートが使えません。
これらのファイルのパスには半角スペースが含まれない様に注意してください。
lintIgnoreExitCode
lint-ignore-exit-codeを見ると、デフォルトが true
となっているので不要かと思いましたが、
この行が無いと エラー扱いになってtextlintのメッセージが一瞬表示された後消えてしまいました。
lintOnSave
バッファで編集中の文章に対しては、textlintが実行できませんので、lintOnSave
を true
にしてあります。
この場合ファイルを開いただけでは textlint が実行されず結果は表示されません。:w
で保存すると textlint が実行されます。
この動作を変更したい場合は lintOnSave を false
にしてください。
rootMakers が効かない
こちらのIssueが私の環境でも起きております。
rootMakersの 一つ目のエントリが '.git/' 以外の場合は project rootの detectに失敗してしまいます。
必ずしもgitで管理しているわけではなく、これだととても作業がしにくいので、代わりに自前で .textlintrc.json を探す様にしています。検索は以下の様に行っています。
- setup handlerが呼ばれたタイミングで、カレントバッファのファイルのあるディレクトリ
- カレントバッファのディレクトリの上位ディレクトリ
- 見つからなければ nvim の設定ディレクトリにある .textlintrc.json
- それも無ければコマンドラインには何も渡さずに textlintを実行 (efm-langserverのrootMarkers次第となります)
rootMarkersの問題が無い場合、または gitプロジェクト以外のtextファイルはlintを通さなくて良い場合は、setup handler中の useRootMarkers を trueにしてください。
rule等の導入
ここは、textlintを masonで導入している方が対象です。
rule等は、textlintの環境に追加する必要があります。
一番簡単なのは、%LOCALAPPDATA%\nvim-data\mason\packages\textlint
に移動して、npm install --save
で導入することかと思います。例えばこんな感じ
cd %LOCALAPPDATA%\nvim-data\mason\packages\textlint
npm install --save textlint-rule-preset-japanese textlint-rule-preset-ja-technical-writing textlint-filter-rule-comments textlint-filter-rule-allowlist
前述の mason-textlint-helper で、TextLintInstall を実行することで
mason-textlint-helperの中に記載されているモジュールを導入することができます。
以下でモジュールの関数をコマンドに登録してやって、:TextLintInstall
で導入されます。
バックグラウンドで導入されるため、実行後しばらくお待ちください。
vim.cmd([[command! TextLintInstall lua require('mason-textlint-helper').TextLintInstall()]])
TextLintInstallでインストール完了時に完了した事を表示していたのですが、} ぐらいしか見えないかもしれません。
(Option) .textlintrc.jsonを編集するコマンドの追加
以下を init.lua
等に追加し、:TextLintEditConfig
を実行することで、現在使われているであろう設定ファイルを開く事ができます。
vim.cmd([[command! TextLintEditConfig lua require('mason-textlint-helper').TextLintEditConfig()]])
(Option) 設定ファイルが見つからない場合のfallback先 .textlintrc.jsonを作成する
nvimを起動したときに、設定ファイルが見つからない場合は、
nvimの設定フォルダにある .textlintrc.json
が読み込まれます。
設定ファイルが見つからない時に textlint を実行しないで良い場合は、このファイルを作成しないでください。
{
"rules": {
"preset-japanese": true
}
}
おわり
思ったより長くなってしまいましたので、時間が足りず推敲できておりませんが リリースしておきます🙇。