2023/12/03

NeovimをさらにLuaLuaさせた

あれから一年近い月日が経った。ひとたび完結を見た僕のinit.luaはその後も進化し続け、ずいぶんIDE的な出で立ちに変貌を遂げた。当初のサブ武器としての位置付けはどこへやら、今ではすっかり長剣の顔をして鞘に収まっている。電脳空間を切り開くデジタルロードアウトはえてして可変長であり、所有者の意向次第で自在に特性を変えられるのだ。本稿では新たに増えたプラグイン群を紹介する。

jaq-nvim

いわゆるタスクランナー。書いたコードを即時に実行してくれる。国内では応用の幅広さからvim-quickrunがとりわけ有名だが、元がVim用のプラグインなのでNeovim特有のUIに対応していない惜しさがあった。jaq-nvimは逆にNeovimでしか動かない代わりにfloat windowで表示できる。

設定では実行したい言語のコマンドを指定するほか、ウインドウの枠や位置を自分の好みに決められる。どうせターミナル上にいるのだからタブを開くなり、tmuxのpaneで別途実行するなりすればいいという意見もあるが(僕も昔はそう思っていた)、多少なりともワンクッションの手間を減らせる効能は意外にあなどれない。以下に設定例を示す。

 1require("jaq-nvim").setup({
 2	cmds = {
 3		internal = {
 4			lua = "luafile %",
 5			vim = "source %",
 6		},
 7		external = {
 8			python = "python3 %",
 9			go = "go run %",
10			sh = "sh %",
11			ruby = "ruby %",
12			java = "java %",
13			javascript = "node %",
14		},
15	},
16
17	behavior = {
18		default = "float",
19		startinsert = false,
20		wincmd = false,
21		autosave = false,
22	},
23
24	ui = {
25		float = {
26			border = "none",
27			winhl = "Normal",
28			borderhl = "FloatBorder",
29			winblend = 0,
30			height = 0.8,
31			width = 0.8,
32			x = 0.5,
33			y = 0.5,
34		},
35
36		terminal = {
37			position = "bot",
38			size = 10,
39			line_no = false,
40		},
41		quickfix = {
42			position = "bot",
43			size = 10,
44		},
45	},
46})
47
48vim.keymap.set("n", "<leader>j", ":<C-u>Jaq<CR>", { silent = true })

なにげにコンパイル言語もすぐに走らせられるので言語学習のお供にも役立つ。

colorful-winsep.nvim

splitしたwindowの枠に色をつける……たったそれだけのことが予想以上の視認性向上をもたらしめるのはまさに目からうろこだった。特にlaststatus = 3にして単一のstatusしか表示させていない人はとても助かるだろう。以下に設定例を示す。

1require("colorful-winsep").setup({
2	highlight = {
3		bg = "",
4		fg = "#E8AEAA",
5	},
6})

実際にはstatusをよく確認すればファイルパスの違いで見分けはつかなくもないが、編集位置を高速で移動している最中に直感を得るのはなかなか難しい。他方、自分の好みの色でハイライトされていると一瞬の躊躇なく迷わずに済む。

nvim-dap

Neovimを含む現代のエディタがLSP(Language Server Protocol)の恩恵を受けているのはよく知られた話だ。いつになく気前のよいMicrosoftがオープンソースで公開してくれているので、我々は種々の仕組みを通してそれらを利用できる。同様に、実はデバッガもDAP(Debug Adapter Protocol)なる仕様が公開されており、今やNeovim上でIDE並みのデバッグ環境が手に入る。

恥ずかしながら僕はVimでデバッグをしようと考えたことがなかったので、初めて知った時はかなり感動した。名実ともにすべての作業がVimで行えるとなれば、大規模開発におけるエディタの立ち位置はますます見直されていくだろう。例によって設定を示す。

 1local function map(mode, lhs, rhs, opts)
 2	local options = { noremap = true }
 3	if opts then
 4		options = vim.tbl_extend("force", options, opts)
 5	end
 6	vim.api.nvim_set_keymap(mode, lhs, rhs, options)
 7end
 8
 9map("n", "<leader>6", ":lua require'dap'.continue()<CR>", { silent = true })
10map("n", "<leader>7", ":lua require'dap'.step_over()<CR>", { silent = true })
11map("n", "<leader>8", ":lua require'dap'.step_into()<CR>", { silent = true })
12map("n", "<leader>9", ":lua require'dap'.step_out()<CR>", { silent = true })
13map("n", "<leader>;", ":lua require'dap'.toggle_breakpoint()<CR>", { silent = true })
14map("n", "<leader>'", ":lua require'dap'.set_breakpoint(vim.fn.input('Breakpoint condition: '))<CR>", { silent = true })
15map(
16	"n",
17	"<leader>i",
18	":lua require'dap'.set_breakpoint(nil, nil, vim.fn.input('Log point message: '))<CR>",
19	{ silent = true }
20)
21map("n", "<leader>d", ":lua require'dapui'.toggle()<CR>", { silent = true })
22map("n", "<leader><leader>d", ":lua require'dapui'.eval()<CR>", { silent = true })

この例では<leader>;などでブレークポイントを指定する。しかし当然、これのみでは肝心のデバッガ本体が存在しないため動かない。LSPクライアントで任意の言語サーバを導入するように、デバッガも各自導入して設定しなければならない。ほぼ入れただけでよしなにやってくれるものもあれば、ひと手間が必要な場合もある。

たとえばデバッガをMasonで管理していて、そっちの方を使いたい時は下記の要領でファイルパスを記さないといけない。nvim-dap-goは書かなくてもPATHが通っている外部のコマンドを勝手に実行してくれるが、nvim-dap-pythonはどちらにしても明記しないと怒られる仕様だった。

 1{ "suketa/nvim-dap-ruby", config = true, ft = "ruby" },
 2{ "leoluz/nvim-dap-go", ft = "go" },
 3{ "mfussenegger/nvim-dap-python", ft = "python" },
 4
 5--nvim-dap-go
 6require("dap-go").setup({
 7	delve = {
 8		path = ".local/share/nvim/mason/packages/delve/dlv",
 9	},
10})
11
12--nvim-dap-python
13require("dap-python").setup(vim.fn.stdpath("data") .. "/mason/packages/debugpy/venv/bin/python")

なお、Javaに至ってはあまりにも設定が面倒臭すぎて動作検証を放棄してしまった。Masonのjdtlsを使ってうまくやれた人がいたら逆に教えてほしい。

nvim-dap-ui

ここまでで一応デバッグは行える形になったが、それにしても画面がおざなりすぎるところは否めない。そこでこのプラグインを入れてやる。すると、keymapに応じてデバッグ専用に設えられたwindowが勇ましくジャキーンと展開される。さながら変形によって破壊力が上がる武器のようだ。ここぞという時に展開して一撃で獲物を仕留める……そんな印象を受ける。

 1require("dapui").setup({
 2	icons = { expanded = "▾", collapsed = "▸", current_frame = "▸" },
 3	mappings = {
 4		expand = { "<CR>", "<2-LeftMouse>" },
 5		open = "o",
 6		remove = "d",
 7		edit = "e",
 8		repl = "r",
 9		toggle = "t",
10	},
11	expand_lines = vim.fn.has("nvim-0.7") == 1,
12	layouts = {
13		{
14			elements = {
15				{ id = "scopes", size = 0.25 },
16				"breakpoints",
17				"stacks",
18				"watches",
19			},
20			size = 40,
21			position = "left",
22		},
23		{
24			elements = {
25				"repl",
26			},
27			size = 0.25,
28			position = "bottom",
29		},
30	},
31	controls = {
32		enabled = true,
33		element = "repl",
34		icons = {
35			pause = "",
36			play = "",
37			step_into = "",
38			step_over = "",
39			step_out = "",
40			step_back = "",
41			run_last = "↻",
42			terminate = "□",
43		},
44	},
45	floating = {
46		max_height = nil,
47		max_width = nil,
48		border = "single",
49		mappings = {
50			close = { "q", "<Esc>" },
51		},
52	},
53	windows = { indent = 1 },
54	render = {
55		max_type_length = nil,
56		max_value_lines = 100,
57	},
58})

設定項目は非常に多い。そのうち細かく変えるつもりであえてデフォルト設定を書き連ねていたが、なんだかんだで大していじっていない。見ての通り、各windowの位置関係やアイコン類を変更できる。僕はREPLの表示領域を広くした。いずれにしてもこれでIDEと同等の作業環境が手に入る。

Lspsaga

本稿のトリを務めるのはLSP関連の機能をリッチにせしめるプラグインだ。UIのみならずNeovim本体では提供していない部分もカバーしてくれる。このプラグインの存在は以前から知っていたものの、使いこなせる自信がいまいち持てず今まで放置していた。だが、一度入れてしまえば案外なんとかなるもので、lsp_linesなどの一部プラグインを統廃合することに成功した。設定例は以下の通り。

 1require("lspsaga").setup({
 2	symbol_in_winbar = {
 3		enable = false,
 4	},
 5	ui = {
 6		border = "single",
 7		title = false,
 8	},
 9	lightbulb = {
10		enable = false,
11	},
12})
13
14local on_attach = function(client, bufnr)
15	client.server_capabilities.documentFormattingProvider = false
16	local set = vim.keymap.set
17	set("n", "K", "<cmd>Lspsaga hover_doc<CR>")
18	set("n", "<leader>1", "<cmd>Lspsaga finder<CR>")
19	set("n", "<leader>2", "<cmd>Lspsaga rename<CR>")
20	set("n", "<leader>3", "<cmd>Lspsaga code_action<CR>")
21	set("n", "<leader>4", "<cmd>Lspsaga show_line_diagnostics<CR>")
22	set("n", "<leader>5", "<cmd>Lspsaga peek_definition<CR>")
23	set("n", "<leader>[", "<cmd>Lspsaga diagnostic_jump_prev<CR>")
24	set("n", "<leaaer>]", "<cmd>Lspsaga diagnostic_jump_next<CR>")
25end
26vim.lsp.handlers["textDocument/publishDiagnostics"] =
27	vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { virtual_text = false })

もっとも、すべての制御をこれに任せるのもかえって冗長なので一部はTelescopeに割り振って機能の分散化を図っている。装飾もなるべく抑えてlightbulbwinbarは無効化している。総合的にはさほどうるさい画面にならず賢くまとまったと思う。

おわりに

やはりLSPやDAP関連の機能は重たいのか、当初は50msを切っていたNeovimの起動速度がついに100msを超えてしまった。遅延処理をぼちぼち施してもせいぜい80ms程度なので、よほど設定を煮詰めないかぎりRyzen 5 5600G程度のCPUパワーではこの辺りが限界なのだろう。とはいえ、それでも本物のIDEと比べれば依然として高速な開発環境には違いない。いつの日か真にすべての開発作業がターミナルの内側で完結することを願う。

©2011 Rikuoh Tsujitani | Fediverse | Bluesky | Keyoxide | RSS | 小説