地味にこだわってるneovimの設定たち

こんにちは
2月に今の案件に入ってからあっという間に3月終わり、時の流れに置いてかれそうな杉浦と言います

自分はエディタにneovimを使っているんですが、いまだに同じneovim使いと直接会ったことがありません
現場でもCodemingleでもneovim使いは自分のみ
ネットを見ていると沢山いるような感じがするんですがね..
そもそも知らないって反応もあるあるですよね

でもそんなのじゃもったいない!
使わなくてもいいから、neovimってエディタ結構面白いぞ!・こんな事も出来るのか!ってのだけでも感じて欲しいと思ったので、Codemingleメンバーの皆様を洗脳する際の資料として、興味を持ってもらうきっかけとして、自分の醸成しているluaコードたちの中からネタになりそうなものをよしなに引っ張ってきました
お納め下さい
では早速見ていきましょー

この記事で書かれている事:
・neovimの設定を書くときに意識していること
・お勧めしたいプラグイン(の設定)

この記事で書かれてない事:
・neovim apiの解説
・neovim vs 他のエディタ

..の前に

まず、個人的にneovimで一番おもろいと思っているluaについて軽く触れます
多分ここがこの記事で一番おもろいです(ぉ

そもそもですが、neovimではluaと言うスクリプト言語を用いて設定を記述していきます
(なんでluaなの?、についてはneovimコントリビュータの一人であるTJ DeVriesさんがご自身のyoutubeチャンネルで熱く語っているので、詳細はここでは触れません)

エディタに限らず多くのソフトウェアは、設定をデータとして解釈するのが一般的かと思います
vscodeなんかもsettings.jsonに書いていきますよね
一方neovimでは”設定”と言うよりは、neovimの振る舞いをluaと言う汎用言語(とneovimのapi)を用いて”プログラム”すると言う方が正確かもしれません
なので、機能上はプラグインと設定の違いはあんまりありません
あるプラグインが提供する機能は、設定ファイルに同じ意味のコードを書けばそのプラグインを入れなくても再現できちゃいます
なんなら、丁度いいプラグインが見つからないから自分で実装しちゃおう!って事も気軽にできます
この設計及び思想のおかげでneovimは異様なカスタマイズ性を実現しています

基本的なキーバインド

esc

コーディング中に頻繁に使うキーバインドってあるじゃないですか
細かい部分は人によって違うと思いますが、cr, tab, escとかは誰しも頻繁に使うと思います
こうゆうキーバインドって無性に機能モリモリにしたいですよね!(ね!)
と言う願望が素直に漏れ出しているキーバインドがこちら

local m = vim.keymap.set
m('i', '<esc>', function()
   vim.cmd 'wa'

   -- ノーマルモードに戻る
   vim.cmd 'stopinsert'

   -- 保存時に自動でフォーマットをかける
   -- language server(に相当するツール)がフォーマット機能を提供している場合のみ
   -- 自動フォーマットする(この判定がないと、特定のバッファでノーマルモードに戻る度にエラーメッセージが表示されて鬱陶しい)
   local cur_buf = vim.api.nvim_get_current_buf()
   local has_format_ability = false
   local lsp_clients = vim.lsp.get_clients { bufnr = cur_buf }
   for _, client in pairs(lsp_clients) do
      if client.server_capabilities.documentFormattingProvider then
         has_format_ability = true
         break
      else
      end
   end
   if has_format_ability then
      vim.lsp.buf.format { async = true }
   end
end)

インサートモードからノーマルモードに戻るキーはデフォルトだとescキーに割り当てられています
キーバインド自体は変更していないのですが、そこに自動保存と自動整形の機能を割り振ってます
また、現在編集中のバッファのみ保存するのではなく、現在開いているバッファ全てを保存できるようにvim.cmd ‘w’ではなくvim.cmd’wa’としています

因みに上の自動整形のコードは不完全な部分があります
フォーマット機能を提供しているlanguage serverがフォーマットしたい言語のものとは限らないからです

これだけだと何を言っているのかあまり伝わらないので具体例を挙げましょう
後述しますが、自分はrustのdoc commentにrender-markdown.nvimのリッチな表示を適用するためにtree-sitterのlanguage injectionというものを利用しています
その為(..なのかどうかは厳密には分かってません)rustファイルを開くとrust-analyzerだけではなく、marksman(markdown用のlsp実装の一つ)も立ち上がります

この様に、あるバッファで立ち上がっているlanguage serverが自分の思っているやつだけではないことがあり得ます
幸いにもrust-analyzerはフォーマット機能を提供しているので特に問題はありませんが、これが例えば「rust-analyzerはフォーマット機能ないけどmarksmanはフォーマット機能を持っている」という様な状態であれば、自分が思ってたのと違う挙動をすることがあり得ます

とはいえその様なケースはレアだと思うので、必要に応じて条件分岐するコードを追加すればいいだけではあります(因みに自分はそれで困った事はないのでそのままにしています)

自分はneovimのターミナルモードをよく利用します
このターミナルモードというのが結構便利で、普通にneovimのバッファの一つとして開くので1つ前のコマンドの出力をコピーしたい時(エラーメッセージをコピペして検索する時とかに重宝しています)や文字列検索やその他色々がいつものキーバインドでできてしまう優れものです
更に、これは個人的に嬉しい点なのですが、cliでコマンドやオプション、履歴の補完を表示してくれるamazon q cliというアプリがあるのですが、neovimのターミナルモードでもコイツが動いてくれるのは意外でした

話は脱線しますがこのamazon q、元々figという名前のアプリだったのですが、どうやらawsに吸収されたらしくその際に名称が変更されています
macのみ対応なのもあって(linuxにも対応予定とどこかで見た覚えはあります)あんまり知名度はないイメージですが、個人的には自分のmacに入っている有能ツールの中でも1,2を争うくらい便利なツールです
一時期asahi linux(asahi-fedora-remix)を入れていた時期もあったのですが、コイツとraycastが現状macでしか使えない為結局macOSに戻ってきてしまうくらいには便利に使わせてもらっています
ありがとうございます😆

..話を戻して、neovimのターミナルモードでは所謂インサートモードから所謂ノーマルモードに戻るキーバインドが<c-\><c-n>に割り当てられています
こんな黒魔術みたいなのがデフォルトってまじ?と思いますが、それ以上に毎回忘れてしまうので

m('t', '<esc>', '<c-\\><c-n>')

この様に、escキーに割り当てています
察するに、ターミナル操作でescを送りたい場合を考慮した結果の黒魔術なのでしょう
ただ、少なくとも自分のケースではescに割り当てても現状困ったことはありません

最後に、esc絡みでもう一つ
(neo)vimには、文字列検索の際にハイライトされた箇所をクリアするnohlsearchというコマンドがあります
これを何処かのキーに割り当てる際に脳死コピペ記事でよく紹介される方法として<esc><esc>に割り当てるというものがあります
ただ、殆どのケースでは↓のように普通に<esc>に割り当てて問題ないかと思います
esc2回押しにするメリットってあるんだろうか🤔

自分の場合は、画面上の通知をクリアする動作も同じキーバインドにしています
こんな感じでキーバインドと機能をリンクさせる(例:ノーマルモードでescを押すと〇〇する)のではなく、キーバインドとイメージをリンクさせる(例:ノーマルモードで〇〇な操作したいときは、escを押す)事を意識すると覚えるものが少なくなりますし、キーバインドのシンプルなモジュール化に繋がりやすいのでお勧めです

m('n', '<esc>', function()
   require('notify').dismiss { pending = true, silent = true }
   vim.cmd 'noh'
end)

^と$

(neo)vimのデフォルトでは、ノーマルモードやビジュアルモードで行頭に移動するキーは^、行末にカーソルを移動するキーは$を使います
個人的にはこの設定が結構ややこしくて、JISはどうか分からないのですがUS配列だとshift + 4が$に、shift + 6が^になっています
キーボード上では左側の$を押すとカーソルが右に動く(vice versa..)とゆう挙動に全く慣れることができず、いつも2秒くらい宇宙ネコになっていたので入れ替えました

m({ 'n', 'x' }, '$', '^')
m({ 'n', 'x' }, '^', '$')


たったの二行ですが、個人的にはこれでだいぶ使いやすくなりました(これをデフォルトにしてくれ..)

cr

リターン(エンター)もよく使うキーだと思います
ざっくりとですが、リターンキーは何かを決定したい時や画面の遷移が伴う時に押すキーというイメージがあります
なので、そのイメージに沿った機能を付け足しています

local function is_url(path)
return path:match '^https?://'
end

m({ 'n', 'x' }, '<cr>', function()
   local special_ft = { 'ssr' }
   local cfile = vim.fn.expand '<cfile>'
   if is_url(cfile) then
      vim.ui.open(cfile)
   elseif table_contains(special_ft, vim.bo.ft) then
      return '<cr>'
   else
      return ':Make '
   end
end, { expr = true })
m({ 'n', 'x' }, '<s-cr>', function()
   if vim.bo.ft == 'ssr' then
      return '<s-cr>'
   else
      return ':!'
   end
end, { expr = true })

インサートモードでは改行として機能して欲しいので弄っていません
ノーマルモードやビジュアルモードでは、状況に応じて3通りの振る舞いをします

一つ目は、カーソルがurlの上にある時
これはvim.fn.expand '<cfile>'を用いて判定しています
この関数の返り値は現在カーソルが何かしらのパスの上にある場合はそのパスを返します
この値をis_url関数でurlかどうかで判定し、urlだった場合はvim.ui.open(cfile)が実行されリンクがブラウザで開かれます
ただ、状況によってはそのまま<cr>を送りたい事もあります
そう言った状況の場合は2つ目、elseif節にマッチさせて<cr>を送る様にしています

最後にそれ以外のケースはMakeという自分で作ったコマンドを実行しています
このコマンドは標準のmakeと似た様な動作をするのですが、

  1. デフォルトのmakeコマンドの挙動があまり好きではない
  2. ファイルタイプによって柔軟にカスタマイズしたい
  3. ビルドやテストの様に頻繁にする動作をエディタからする場合に、エディタの外部の要因(Makefile)が絡むのはなんか避けたい
  4. そもそもMakefileをプロジェクトを作る度に用意するのが面倒

という理由で自作の方を好んで使っています
2番目はデフォルトのmakeコマンドでもできると言えば出来るのですがコマンド実行時の挙動を制御しようとすると自分で作った方が楽だなと判断しました
自作Makeコマンドの実態はファイルタイプ毎にコンパイラなりインタプリタなりを実行しているだけなので実装は割愛します

また自分の場合は’:!’でシェルコマンドを実行することがよくあるのですが、自分の中で意味的に’:Make’を実行するのと近い行為に感じているので<s-cr>を割り当てています
因みにvim.keymap.setの第三引数にはオプションを指定することができるのですが、exprをtrueにしてやると関数実行後に戻り値の文字列をコマンドと解釈して実行してくれます
条件によっては特別な処理がしたいが、それ以外はデフォルトの挙動にフォールバックしたい時などに便利です

emacsキーバインド

m('!', '<c-a>', '<home>')
m('!', '<c-e>', '<end>')
m('!', '<c-k>', '<right><c-c>v$hs')
m('!', '<c-u>', '<c-c>v^s')
m('!', '<a-d>', '<right><c-c>ves')
m('!', '<a-f>', '<c-right>')
m('!', '<a-b>', '<c-left>')

これは定番ですが文字を入力している最中にノーマルモードにいちいち戻る程ではないがカーソルを移動させたいというのは往々にしてあります
そんな時に基本的なemacsキーバインドを設定してやると便利です
と言いつつ、<c-p, n, f, b>は何も設定していませんが、これはneovim以外でも使いたいキーバインドだったのでkarabiner-elementsの方で設定しています
vim.keymap.setの第一引数に’!’を渡していますがこれは{ 'i', 'c' }と同義です

nvim-cmp

自分は自動補完プラグインにnvim-cmpを使っています
補完プラグインはいくつか試しましたが、nvim-cmpが補完エンジンを提供し必要に応じて補完ソースを追加していくスタイルが自分に合っていたので使い続けています

補完ウィンドウ

local abbr_width = 25
local menu_width = 35
cmp.setup {
   formatting = {
      expandable_indicator = true,
      format = function(entry, vim_item)
         vim_item.abbr = my_str.truncate_end(vim_item.abbr, abbr_width)
         vim_item.menu = my_str.truncate_end(vim_item.menu, menu_width)
         return lspkind.cmp_format { mode = 'symbol', before = require('tailwind-tools.cmp').lspkind_format }(
            entry,
            vim_item
         )
      end,

nvim-cmpではformattingオプションで補完ウィンドウの表示を調整できます
デフォルトでは可能な限り補完メニューを広く取る挙動をします
そのままでも良いのですが、rustの様な型い言語では関数シグネチャがながーーーーーーーーーーーーーーくなる事が往々にしてあります
そうなると、補完メニューの横に表示されるドキュメントウィンドウがペラペラになってしまいます
殆どの場合、完全なシグネチャはドキュメントウィンドウにも書かれていますし、なんならそっちの表示の方が色ついてて見やすい..ということで
補完メニューの幅を60(25+35)に制限しています

匿名関数内で出てくるmy_str.truncate_endは自分が用意したちっこいapiの一部で、第一引数、textに渡した文字列が第二引数、widthの長さより長い場合、後ろに…をつけた状態でwidthの長さに丸める処理をします
実装は以下の様になっています

---@param text string|nil
---@param width integer
---@return string truncated
mod.truncate_end = function(text, width)
   local truncated = ''
   if text ~= nil then
      truncated = string.sub(text, 0, width)
   end
   if truncated ~= text then
      truncated = string.sub(truncated, 0, width - 1) .. ''
   end
   return truncated
end

因みにこのプラグイン、製作者の方が日本人でこんな記事を書いてたりします
読んでみると面白いですよー

補完キーバインド

再びキーバインドのこだわり紹介に戻ります
nvim-cmpでは補完に使うキーバインドをnvim-cmpの設定内に記述する方法をとっています
補完操作も頻繁に行う動作なので色々試行錯誤をした結果、以下の様になりました

mapping = {
   ['<tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
         cmp.confirm { behavior = cmp.ConfirmBehavior.Insert, select = true }
      else
         fallback()
      end
   end, { 'i', 's', 'c' }),
   ['<c-h>'] = cmp.mapping(function(fallback)
      if ls.expand_or_locally_jumpable() then
         ls.jump(1)
      else
         fallback()
      end
   end),
   ['<s-bs>'] = cmp.mapping(function(fallback)
      if ls.expand_or_locally_jumpable() then
         ls.jump(-1)
      else
         fallback()
      end
   end),
},

方針としては、選択にtabは使わない・決定にcrを使わない・delete(mac以外だとbackspaceに当たります)を泳がせているのでうまい具合にポジションを与えたい、の3つです
なぜこうしているかというと、選択にtabを使うより<c-n>, <c-p>を使った方が上下双方向の選択が素直に感じるのと、crは普通に改行に使いたいから、文字削除はkarabiner-elementsを使って<c-h>に割り当てているのでdeleteを遊ばせているのが勿体無いから、です
具体的に見ていきましょう

tabは選択した補完を入力する役割にしました
選択のイメージがあるtabですが他に良いマッピングが見つからなかったのでこの形に落ち着いています

また、deleteキーを押すと、karabiner-elementsによって<c-h>に変換されてターミナルに送られます
カーソルをジャンプさせられる場合はジャンプ位置までカーソルが移動します
ジャンプ位置って基本的に左から右の順に配置されているので、ジャンプをすると基本的に左から右にカーソルが動きます
この動作がキーボード右端にあるdeleteキー(中身は<c-h>)を押してトリガーされるのが直感的で意外と気に入っています

deleteキーとshiftを同時押しすると逆方向にジャンプします

telescope.nvim

neovimでファジーファインダーといったらコレ!なtelescope.nvimですが、デフォルトの時点で相当使いやすくbuiltinの機能も豊富なので正直あんまり設定をこだわってはいないプラグインです
なので語る事もあんまり無いのですが、telescopeの拡張プラグインで相当便利に使わせてもらっているものが1つあって、尚且つお勧めしている記事を全然見た事がないので紹介します

smart-open.nvim

一言で説明するならば、telescopeのUIを使ったfuzzy file finderです
このプラグインはその要となるfuzzyさを実現する為に、他のfuzzy finderとはちょっと違うアイデアを持っています
smart-open.nvimのREADMEにはその点が簡潔に述べられています


In a way, but most other solutions require multiple mappings to search:git files
open buffers
recent files

The goal of smart-open is to give you highly relevant results with as few keystrokes as possible–so much so that only a single mapping is needed for searching everything while still managing to be quick about it.

smart-open.nvimでは、バッファの有無やファイルを開く頻度、現在のパスからの近さなどもfuzzy matchに使い、かなりスマートにサジェストしてくれます
ファイル検索のアルゴリズムって一個一個は便利なんだけど種類が多くて使い分けるの面倒だな..とか、マッピング結構消費するな..というのはおそらく誰もが思ったことがあるのではないでしょうか
自分もその辺に煩わしさを感じていて、色々漁っているうちに出会ったのですが、これを入れてからは他のpickerをほとんど使わなくなりました
脳死で適当に検索できるのがfuzzy finderの良いとこなのに、fuzzy finderを開くまでが脳死でできないというある種のパラドクスを解決してくれるという点で、telescopeのファイル検索拡張系で一番お勧めです✨

LuaSnip

neovimのスニペット系プラグインは色々ありますが、LuaSnipは恐らくその中で一番高機能で使いやすいんじゃ無いでしょうか
何よりドキュメントがかなり充実しているのでわからない事があっても安心です
高機能すぎて持て余してる感じがあったので、少し前に「これLuaSnipじゃないと出来ないだろ〜(知らんけど)」なスニペットを作ってみました
割と便利に使っているのでご紹介

s("cb", {
	d(1, function(_)
		local cmt_map = my_lua_api.comment_indicators(vim.bo.comments)
		local all = { cmt_map.doc.block.outer }

		if all[1].pre ~= cmt_map.doc.block.inner.pre then
			all[2] = cmt_map.doc.block.inner
		end
		if
			all[1].pre ~= cmt_map.normal.block.pre
			and all[2].pre ~= cmt_map.normal.block.pre
		then
			all[3] = cmt_map.normal.block
		end
   
   	local choice_arg = {}
		for idx, p in pairs(all) do
			choice_arg[idx] = t(p.pre)
		end

		local expand_at_new_line = false

		local pbpaste_hndlr = io.popen("pbpaste", "r")
		---@type string
		local pbpaste = ""
		if pbpaste_hndlr ~= nil then
			pbpaste = pbpaste_hndlr:read("*a")
		end

		if pbpaste:find("\n") then
			expand_at_new_line = true
			if pbpaste:sub(1, 1) == "\n" then
				pbpaste = pbpaste:sub(2)
			end
			if pbpaste:sub(#pbpaste) ~= "\n" then
				pbpaste = pbpaste .. "\n"
			end
		end

		local lines = {}
		local lines_with_nl = { "" }
		local idx = pbpaste:find("\n")
		while idx do
			local sub = pbpaste:sub(1, idx - 1)
			lines[#lines + 1] = sub
			lines_with_nl[#lines_with_nl + 1] = sub
			pbpaste = pbpaste:sub(idx + 1)
			idx = pbpaste:find("\n")
		end
		lines_with_nl[#lines_with_nl + 1] = ""

		local yanked_list = {}
		if expand_at_new_line then
			yanked_list[1] = t(lines_with_nl)
			yanked_list[2] = t(lines)
		else
			yanked_list[1] = t(lines)
			yanked_list[2] = t(lines_with_nl)
		end
		yanked_list[3] = t("")
		local yanked_code = c(1, yanked_list)
		
   	local pre_idx = 2
		local cb_pre = c(pre_idx, choice_arg)
  		local cb_post = f(function(pre)
			local post = ""
			for _, p in pairs(all) do
				if pre[1][1] == p.pre then
					post = p.post
					break
				end
			end
			return post
		end, { pre_idx })
   	return sn(nil, { cb_pre, yanked_code, cb_post })
	end, nil),
}),

コードの解説の前に、何がしたいのかを説明します
自分はコメントアウトする時にline commentよりもblock commentを使いたいのですが、世にあるコメント強化系のプラグインてほとんどline commentを使うじゃないですか
探せば自分の趣向に合ったものが見つかるかもしれないですが、先述のモチベーションもあって自作するに至りました

使い方としては、コメントアウトしたい部分を選択し切り取った後、`cb`と打って展開すればコメントアウトできます(cbはcomment blockの意)
最初の行でmy_lua_api.comment_indicatorsとありますが、これは、vim.bo.commentsを解析し「これはモジュール内部に書くタイプのdoc commentでかつblock commentだ!(rustの/*!など)」みたいな判定を行い、各コメントの種類に対応する文法を格納したテーブルを返す関数です
LuaSnipの設定以外にも使っているのでapi化しています

その後の流れは行数こそ多いですが割と単純、クリップボードからコピーした内容を拾ってきてスニペットを作成しています
doc commentにするか普通にコメントアウトするかを選択できる様にしたかったのと、普通にコメントブロックの文法のみ欲しい場合のあるので
クリップボードの内容を貼り付けるかとコメントの文法の種類を選択できる様にしたら長くなっちゃいました

初めはコメントアウト用のスニペットとして書いたつもりだったんですが、最近はブラウザでコピーした参考リンクを貼り付ける用途でよく使っています(ぉ

hydra.nvim

neovimではデフォルトでも色々なモードがありますが、hydra.nvimはそれに加えて新しくモードを作れちゃうプラグインです
自分はよく使うコマンドを呼び出せるコマンドパレット的なものが欲しかったのと、ウィンドウ操作用のモードが欲しかったので入れています
例として、ウィンドウ操作用の設定を紹介します

local h = require 'hydra'

~~~

h {
   name = 'window',
   mode = { 'n', 'x' },
   body = '<tab>',
   config = { hint = { type = 'window', position = 'middle' } },
   hint = [[_q_ _w_ cycle
_h_ _j_ _k_ _l_ focus
_H_ _J_ _K_ _L_ move
_<c-b>_ _<c-n>_ _<c-p>_ _<c-f>_ resize
_t_ split new tab _c_ close _x_ exit
]],
   heads = {
      { 'q', '<c-w>W' },
      { 'w', '<c-w>w' },
      { 'h', '<c-w>h' },
      { 'j', '<c-w>j' },
      { 'k', '<c-w>k' },
      { 'l', '<c-w>l' },
      { 'H', '<c-w>H' },
      { 'J', '<c-w>J' },
      { 'K', '<c-w>K' },
      { 'L', '<c-w>L' },
      { '<c-b>', '<c-w><' },
      { '<c-n>', '<c-w>+' },
      { '<c-p>', '<c-w>-' },
      { '<c-f>', '<c-w>>' },
--{ 't', '<cmd>tab split<cr>' },
      {
         't',
         function()
            local win = vim.api.nvim_get_current_win()
            local buf = vim.api.nvim_get_current_buf()

            -- saves the buffer
            vim.api.nvim_buf_call(buf, function()
               vim.cmd 'update'
            end)

            -- split window
            vim.api.nvim_win_close(win, true)
            vim.cmd 'tabnew'
            vim.api.nvim_set_current_buf(buf)
         end,
      },
      { 'c', 'ZZ' },
      {
         'x',
         function()
            vim.cmd 'wqa'
         end,
      },
   },
}

body = '<tab>'の部分でモードのトリガーを設定できます
因みにtabにしているのは、ノーマルモードで余ってるキー…tabや!、っていう軽いノリで決めてます
機能はシンプルで、フォーカス移動・ウィンドウ移動・サイズ調整 etc..ができるよって感じです

tの部分で何をしているかというと、現在のウィンドウを新しいタブに移動させる処理をしています
なんかこのウィンドウ使わんし邪魔になってきたけと閉じたくはないな..って時に<tab> + tで別のタブに逃すっていう使い方をしています

ftplugin

ファイルタイプ毎に特定の設定をしたい時ってあると思います
そんな時、auto commandを使う方法とは別に、ftpluginという仕組みも(neo)vimでは提供されています
例えば、init.luaから見てafter/ftplugin/の場所に<ファイルタイプ名.lua>を置くと、そのファイルタイプのバッファに入った際、グローバルプラグインをロードした後にその設定を読み込んでくれます

例えば、自分は↓の内容のmarkdown.luaを置いてます

vim.bo.comments = vim.bo.comments .. ',s:,b:-,b:> ,b:1. '

ここでは、先程紹介したvim.bo.commentsを解析する関数に渡す前に、markdownでのコメントを付け加える作業をしています
こんな感じでftpluginは痒い所に手が届く孫の手的な存在です

language injection

reactのjsx/tsxやmarkdownの様に、一つのファイルに複数の言語(風)の文法が存在する事があります
Rustのdoc commentなんかもそうですね
そんな時に、tree-sitterの機能としてパースされたノードに他の言語を注入する事ができます
読んで字の如くlanguage injectionと呼ばれています

基本的にはデフォルトの設定で問題ないのですが、冒頭で触れたようなRustのdoc commentでrender-markdownを動かしたい!といったニッチなケースでは自分で注入する必要があります

と言ってもやり方は簡単
まず、init.luaから見てafter/queries/<filetype>/の場所にinjections.scmを置きます
injections.scmの内容はやりたい事によって様々ですが、上記のケースの場合はこんな感じ

;; extends
((doc_comment) @injection.content
   ;(#set! injection.combined)
   ;(#set! injection.include-children)
   (#set! injection.language "markdown"))

内容を要約するとdoc_commentノード内はmarkdownです!って言ってます
そのまんまですね

最初に、;; extendsという記述がありますが、これは自分でtree-sitterの構文解析を上書きする時に必要になるものです
要はqueries/配下においているファイルはこいつを頭につけないと影響が反映されません
後は、注入したい部分、この場合はdoc_commentに@injection.contentと宣言してそれ以降にどの様に注入するのかを指定してやります
(#set! injection.combined)(#set! injection.include-children)をコメントアウトしていますが、これは挙動が良くわからなかった為です
何故か、rustコードの部分もmarkdown構文として認識してしまう..

オプション

ここまでキーバインドやプラグインの設定など、割とlua luaしている部分を見てきましたが、それ以外でこだわっている部分としてインデントがあります

vim.bo.shiftwidth = 3
vim.bo.tabstop = 3
vim.bo.softtabstop = 3
vim.opt.list = true
vim.opt.listchars = { tab = '' }

自分はインデント3文字派なのでvim.bo.shiftwidthvim.bo.tabstopvim.bo.softtabstopを3に設定しています
インデント文字は好みでtab文字にしています

実は(neo)vimではプラグインを入れなくとも所謂indent lineを表示する事が可能です
vim.opt.listとすると、spaceやtabの様な特殊文字の見た目を装飾できます
vim.opt.listcharsにその具体的な内容を記述してやります

自分がindent line系のプラグインを入れない理由は、あの手のプラグインって基本的にインデント4文字を前提としているため3文字インデントにしていると表示がぐちゃぐちゃになるんですよね..
プラグイン入れなくても2行で同じ効果が得られるならそれでいーじゃんという事で自分で設定しています(インデント3文字流行れ)

デフォルトファイル

neovimを開く際にファイルを指定せずに開くとスタートページ?の様な特殊なバッファが開くかと思います
ここの表示をリッチにするプラグインはよく見かけるのですが、自分はinit.luaが開く様にしています
shellの設定ファイル(自分はzshを使っているので.zshrc)でnvimをnにアライアスしているので、ターミナルでnと一文字打つだけで現在のディレクトリに関係なくinit.luaが開きます
やり方は簡単で、以下の処理をinit.luaの末尾に付け加えるだけ

if vim.fn.expand '%:p' == '' and vim.bo.ft ~= 'lazy' then
   vim.cmd 'e $MYVIMRC'
end

このようにvim.fn.expand'%:p'で現在のバッファのパスが取得できるので、起動時にそれが空の場合はinit.luaを開くという仕組みです
後はお好みでinit.luaを開く条件変更したり、処理を追加したり.. やっぱりこういう事ができるのって楽しいですね

おしまい

PS. 最近よくRustのproc macroを書く機会があるのですが、4ヶ月前は苦戦していたproc macroが今はスラスラかけていて自分でも意外でした
自分の成長ってある程度客観的なデータがないと実感しづらいですよね ほな0w0ノシ

杉浦 寛行

 杉浦 寛行

スキル:Rust/Lua/Java/C/C++/Lisp/TypeScript/WebAssembly/Assembly/osdev/Neovm/nix/AWS/Linux

コメント

この記事へのコメントはありません。

関連記事