インフラ系専門学生の日記Privacy Policy

Neovimでinit.luaを使う

この記事は、ITRCアドベントカレンダー2020の21日目です。

前日はrin1208さんの「Makefileのすゝめ」です。

明日はkoさんの「tmux導入してみた。」です。

目次

動機

最近、Neovimで init.vim の代わりに init.lua が使えるようになったので、試しにやってみたかった。

ちなみに「やってみた」だけで実用性はありません。というかまだ実用の域に達してないというのが実際の所です。

話の流れ

従来、Neovimの設定は init.vim に書くのが習わしでした。

この init.vim というのは、Vim Scriptという言語で書かれており、これがあまり書きやすくない(主観)し、処理速度に優れている訳でもない。

そんな中、NeovimではLuaという軽量スクリプト言語のランタイム(LuaJIT)を内蔵し、Vim Scriptの代わりにLuaでVim Script相当の処理を記述する事が可能になりました。

ここまでは以前の話。

最近マージされたPRにより、 ~/.config/nvim/init.lua として設定ファイルを配置できるようになりました。

なので、 init.lua を書いてみる事にしました。

ちなみに init.luainit.vim は同時に存在できません。リネームするなり移動するなりしてください。

実際書いたコード

~/.config/nvim/init.lua

-- Load Modules
require('core.init')

Neovimの仕様として、 runtimepath 配下の lua ディレクトリ内をルートディレクトリとして *.lua のパスを解決してくれます。

なので、この require では ~/.config/nvim/lua/core/init.lua を読み込んでいる事になります。

~/.config/nvim/lua/core/init.lua

local api = vim.api

vim.cmd('augroup MyAutoCmd')
vim.cmd('autocmd!')
vim.cmd('augroup END')

vim.cmd('filetype off')
vim.cmd('syntax off')

local vars = {
  python_host_prog = '/usr/bin/python2',
  python3_host_prog = '/usr/bin/python3',
  loaded_matchparen = 1
}

for var, val in pairs(vars) do
  api.nvim_set_var(var, val)
end

require('core.options')
require('core.keys')
require('core.ime')
require('plugins.dein')

vim.cmd('filetype plugin indent on')
vim.cmd('syntax on')

vim.api はただのテーブルなので、local api = vim.api とする事でエイリアス的に使う事ができます。

autocmd syntax 周りのapiはまだ整備されていないので、直接Vimのコマンドを叩いています。

現状読みやすさ重視で vars に連想配列をブチ込んでforを回す方式を取っています。もっといいやり方があれば教えてください。

~/.config/nvim/lua/core/options.lua

オプション周りの設定です。

実はNeovimのLuaにはVim Scriptで言う所の :set に相当するオプションが無く、バッファローカル、ウィンドウローカルのオプションのデフォルト値の設定ができません。

これが何を意味するかと言うと、 fern.vimNERDTree を使った時に、バッファローカルのオプションが消えてしまいます。

ここではそれを解決するために、オプションの設定を関数化し、 AutoCmd で毎回フックするようにしてあります。

正直ここがあるので init.lua を勧められないというのがあります。

function SetOptions()
  local api = vim.api

  local opts = {
    splitright = true,
    splitbelow = true,
    clipboard = 'unnamedplus',
    hlsearch = true,
    mouse = 'a',
    whichwrap = 'b,s,h,l,<,>,[,]',
    ignorecase = true,
    smartcase = true,
    pumheight = 10,
    lazyredraw = true,
    showcmd = false,
    guicursor = vim.o.guicursor..',a:blinkon0',
    encoding = 'utf-8',
    undodir = vim.env.HOME..'/.local/share/nvim/backup',
    termguicolors = true
  }

  local wopts = {
    cursorline = true,
    signcolumn = 'yes',
    number = true,
    foldmethod = 'marker'
  }

  local bopts = {
    autoindent = true,
    smartindent = true,
    tabstop = 2,
    shiftwidth = 2,
    expandtab = true,
    undofile = true
  }

  for opt, val in pairs(opts) do
    vim.api.nvim_set_option(opt, val)
  end

  for opt, val in pairs(wopts) do
    vim.api.nvim_win_set_option(0, opt, val)
  end

  for opt, val in pairs(bopts) do
    vim.api.nvim_buf_set_option(0, opt, val)
  end
end

SetOptions()

vim.cmd('autocmd FileType * lua SetOptions()')

~/.config/nvim/lua/core/keys.lua

vim.g.mapleader = " "

local api = vim.api

api.nvim_set_keymap('n', 'x', '"_x', { noremap = true })
api.nvim_set_keymap('n', 's', '"_s', { noremap = true })
api.nvim_set_keymap('v', 'x', '"_x', { noremap = true })
api.nvim_set_keymap('v', 's', '"_s', { noremap = true })
api.nvim_set_keymap('n', '<CR>', ':<C-u>call append(".", "")<CR>', { noremap = true, silent = true })
api.nvim_set_keymap('n', '<Tab>', ':bnext<CR>', { noremap = true, silent = true })
api.nvim_set_keymap('n', '<S-Tab>', ':bprevious<CR>', { noremap = true, silent = true })
api.nvim_set_keymap('n', 'j', 'gj', { noremap = true })
api.nvim_set_keymap('n', 'k', 'gk', { noremap = true })
api.nvim_set_keymap('n', 'i', 'len(getline(".")) ? "i" : "cc"', { noremap = true, expr = true })
api.nvim_set_keymap('n', 'A', 'len(getline(".")) ? "A" : "cc"', { noremap = true, expr = true })
api.nvim_set_keymap('n', 'Y', 'y$', { noremap = true })
api.nvim_set_keymap('n', '0', "getline('.')[0 : col('.') - 2] =~# '^\\s\\+$' ? '0' : '^'", { noremap = true, expr = true })

vim.g.mapleader = " " とする事でスペースキーをleaderに割り当てています。 "<Space>" だとうまくいかないので注意。

他はそのままですね。キーマップ周りを書きやすくするプラグインもあるらしいですけどここでは未使用。

~/.config/nvim/lua/core/ime.lua

local api = vim.api

function ImeOff()
  os.execute('fcitx-remote -c')
end

function ImeOn()
  os.execute('fcitx-remote -o')
end

function ImeStat()
  local imests = io.popen('${HOME}/.config/nvim/imests')
  return imests:read()
end

function ImeAutoOff()
  api.nvim_win_set_var(0, 'ime_status', ImeStat())
  ImeOff()
end

function ImeAutoOn()
  if not (vim.fn.exists('w:ime_status') == 1) then
    api.nvim_win_set_var(0, 'ime_status', '0')
  end
  if api.nvim_win_get_var(0, 'ime_status') == '1' then
    ImeOn()
  end
end

vim.cmd('augroup InsertHook')
vim.cmd('autocmd!')
vim.cmd('autocmd InsertLeave * lua ImeAutoOff()')
vim.cmd('autocmd InsertEnter * lua ImeAutoOn()')
vim.cmd('augroup END')

Vimのモードという概念とIMEの相性が良くないのは皆さんご存知でしょう。

どこかの記事で見つけた解決策(IMEの状態を保存して InsertLeave InsertEnter でフックする)をLuaに書き直してあります。

fcitx-remote を使っているのでWindowsやmacには非対応です。

~/.config/nvim/lua/plugins/dein.lua

local api = vim.api

local dein_dir = vim.fn.expand('~/.local/share/nvim/dein')
local dein_repo_dir = dein_dir..'/repos/github.com/Shougo/dein.vim'

api.nvim_set_var('dein#install_github_api_token', os.getenv('DEIN_GITHUB_TOKEN'))

if not string.find(api.nvim_get_option('runtimepath'), '/dein.vim') then
  if not (vim.fn.isdirectory(dein_repo_dir) == 1) then
    os.execute('git clone https://github.com/Shougo/dein.vim '..dein_repo_dir)
  end
  api.nvim_set_option('runtimepath', dein_repo_dir..','..api.nvim_get_option('runtimepath'))
end

if (vim.fn['dein#load_state'](dein_dir) == 1) then
  vim.fn['dein#begin'](dein_dir)
  local rc_dir = vim.fn.expand('~/.config/nvim')
  local toml = rc_dir..'/dein.toml'
  local lazy_toml = rc_dir..'/dein_lazy.toml'
  vim.fn['dein#load_toml'](toml, { lazy = 0 })
  vim.fn['dein#load_toml'](lazy_toml, { lazy = 1 })
  vim.fn['dein#end']()
  vim.fn['dein#save_state']()
end

if (vim.fn['dein#check_install']() ~= 0) then
  vim.fn['dein#install']()
end

local removed_plugins = vim.fn['dein#check_clean']()
if vim.fn.len(removed_plugins) > 0 then
  vim.fn.map(removed_plugins, "delete(v:val, 'rf')")
  vim.fn['dein#recache_runtimepath']()
end

dein.vim使いなので、deinのイニシャライズ周りをLuaで書きました。

まとめ

完全に動作を検証できている訳ではないのでバグがある可能性もあります。

LuaとVim Scriptどちらにしても中途半端な知識しかないので、割と苦労しながら書き直しました。

多分これでちゃんと動くはずですが、もう少しLuaの勉強をして書き直したいところ。

僕が使っているNeovimの設定ファイルはGitHubに上がっているので、よければ参考にしてやってください。