dein.vim を Neovim (Lua) で快適に使う

2023/03/13に公開

はじめに

今回は、Lua で設定を書く Neovim ユーザーのための dein.vim テクニックを紹介していこうと思います。

私は Neovim メインで Vim は殆ど使っていません。
このようなユーザーは流行りに敏感(新しいもの好き?)な傾向があるように思います。
プラグインマネージャーだと packer.nvim、最近だと lazy.nvim が人気ですよね。

私も以前は init.lua & packer.nvim を使っていましたが、今は init.vim & dein.vim です。
dein.vim でも快適に Lua を使えるんだぞ、というところを見せていこうと思います。

toml を使いこなす

dein.vim を敢えて選ぶ理由となるのは、やはり toml で設定を書ける点だと思います。
私の設定から一部引用しましょう。

[[plugins]]
repo = 'yuki-yano/fuzzy-motion.vim'
depends = ['denops.vim', 'kensaku.vim']
on_cmd = 'FuzzyMotion'
hook_add = '''
    nnoremap ss <Cmd>FuzzyMotion<CR>
    let g:fuzzy_motion_matchers = ['fzf', 'kensaku']
'''

このように、プラグインの管理とその設定をひとまとめにできるのが便利ですよね。

hook_* と lua_*

まず注目したいのは、hook 機能です。
これには3種類ありまして、hook_add, hook_source, hook_post_sourceです。
それぞれ簡単に説明しますと

  • hook_add: 起動時に実行されます。
  • hook_source: プラグインが読み込まれる直前(runtimepathに追加された後)に実行されます。
  • hook_post_source: プラグインが読み込まれた直後に実行されます。

いやいや、これくらいは知ってるよ、と思ったでしょうか?
実はこれらの他に、lua_* というものもあります。
hook_* のそれぞれに対応していまして、lua_add, lua_source, lua_post_source があるわけです。
hook_* + lua <<EOF でも書けますが(内部的には同じ)、スマートで嬉しいですね。
また、こちらの方が後述する foldexpr のテクニックと相性が良いです。

使用例です。

[[plugins]]
repo = 'uga-rosa/ccc.nvim'
on_event = 'BufRead'
hook_add = '''
    nnoremap <C-c> <Cmd>CccPick<CR>
'''
lua_post_source = '''
    require("ccc").setup({
      default_color = "#40bfbf",
      bar_char = '█',
      point_char = '|',
      point_color = "#40bfbf",
      highlighter = {
        auto_enable = true,
        lsp = true,
      },
    })
'''

Lua は、簡単な設定を書く上では冗長になりやすいです。
dein.vim なら、手軽に Vim script と Lua の良いとこ取りができます。

treesitterで綺麗な syntax highlight を

みなさん、treesitter は使っているでしょうか。
query を書くことで埋め込まれた別言語の syntax highlight が実現できます。
自分で書くのは大変でしょうから、私の使っている query ファイルを置いておきます。
同じディレクトリ構造にして、両方のファイルを配置してください。

綺麗ですね。

foldexpr で見やすく

さて、勘の良い読者はもう気付いているでしょうか。
ここまで私が貼った toml の設定は、何故か hook_*lua_* の中身がインデントされていますね。

これには理由があります。
それはずばり、肥大化した設定を自動で折り畳むためです。
私は dein.vim の ftplugin 機能を使って、toml の時に以下の設定を読み込むようにしています。

setl foldmethod=expr
setl foldexpr=<SID>fold_expr(v:lnum)

function! s:fold_expr(lnum) abort
  let line = getline(a:lnum)
  return line ==# '' || line[0:3] ==# '    '
endfunction

この設定により、全体の見通しが劇的に改善しました。

vim-partedit で LSP の恩恵を受ける

ここまでは見た目の話が多かったので、次はコーディング補助の話をしましょう。
正直に言って、Lua という言語自体はそこまで書きやすいものではありません。
その書き心地において、language server の影響は無視できないでしょう。

toml ファイルに埋め込んでいる以上、diagnostic などを適切に出すのは不可能に思えるでしょうか。

その解決策になるのが vim-partedit です。
このプラグインは、バッファの一部分を切り出して編集し、変更を元のバッファに反映することができます。

context_filetype.vim を組み合わせることで、簡単に hook_*lua_* の範囲を vim-partedit に渡すことができます。

<C-p> で起動し、Q で閉じる設定です。

[[plugins]]
repo = 'thinca/vim-partedit'
on_cmd = 'Partedit'
on_func = 'partedit#start'
hook_add = '''
    let g:partedit#prefix_pattern = '\s*'
    let g:partedit#auto_prefix = 0
'''
[plugins.ftplugin]
toml_markdown = '''
    nnoremap <buffer> <C-p> <Cmd>call <SID>operator_partedit()<CR>
    function! s:operator_partedit() abort
      let context = context_filetype#get()
      if context.range == [[0, 0], [0, 0]]
        echohl WarningMsg
        echomsg 'Context is not found'
        echohl NONE
        return
      endif
      call partedit#start(context.range[0][0], context.range[1][0],
            \ {'filetype': context.filetype})
      nnoremap <buffer><nowait> Q <Cmd>w<CR><Cmd>ParteditEnd<CR>
    endfunction
'''
  • 追記

context_filetype.vim のデフォルト設定では、カーソル上下200行までしか探しません。
ひとつのフック内で200行以上の設定を書く場合は設定しておきましょう。

let g:context_filetype#search_offset = 500

treesitter を使って検出する方法もあります。
nvim-treesitter-clipping がおすすめです。
上記と同様の挙動になる設定です。

[[plugins]]
repo = 'thinca/vim-partedit'
on_cmd = 'Partedit'
hook_add = '''
    let g:partedit#prefix_pattern = '\s*'
    let g:partedit#auto_prefix = 0
'''

[[plugins]]
repo = 'monaqa/nvim-treesitter-clipping'
depends = ['nvim-treesitter', 'vim-partedit']
on_lua = 'nvim-treesitter-clipping'
lua_add = '''
    vim.api.nvim_create_autocmd("FileType", {
      pattern = { "toml", "markdown" },
      callback = function()
        vim.keymap.set("n", "<C-p>", function()
          require("nvim-treesitter-clipping.internal").clip()
          vim.keymap.set("n", "Q", "<Cmd>w<CR><Cmd>ParteditEnd<CR>", { buffer = true })
        end, { buffer = true })
      end,
    })
'''

null-ls で stylua を使っている方向けの追加情報

partedit のバッファでは、残念ながら null-ls が動きません。
どうやら null-ls は、バッファ名が実在しないファイルだと起動しないようです。
私は :%!stylua - に分岐させることにしました。

---@return boolean
local function is_active_null_ls()
  for _, client in ipairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
    if client.name == "null-ls" then
      return true
    end
  end
  return false
end

api.nvim_create_user_command("Format", function()
  if not is_active_null_ls() and vim.bo.filetype == "lua" then
    vim.cmd("KeepCursor %!stylua -f ~/.config/stylua.toml -")
  else
    vim.lsp.buf.format()
  end
end, {})
" plugin/vimrc.vim
command! -nargs=+ KeepCursor call vimrc#keep_cursor(<q-args>)

" autoload/vimrc.vim
function vimrc#keep_cursor(cmd) abort
  let curwin_id = win_getid()
  let view = winsaveview()
  try
    execute a:cmd
  finally
    if win_getid() == curwin_id
      call winrestview(view)
    endif
  endtry
endfunction

終わりに

私は toml による管理がかなり性に合っていまして、なんとか快適に Lua を使えるようにしようと試行錯誤してきました。
この記事を読んだ皆さんも、快適な Lua in toml 生活を送りましょう!

GitHubで編集を提案

Discussion