今夜はminiとsnacksで決まり!

この記事はVim駅伝2025年2月26日(水)の記事です。
前回の記事は mityu さんの「:QuickRun で Vim9 script の実行を簡単にするプラグイン作った」という記事でした。
次回の記事は 2月28日(金) に投稿される予定です。

はじめに

最近、巷で話題になっているNeovimプラグインといえば?そう、「folke/snacks.nvim」(以降、snacks.nvim)です。
色々(30個、2025/02/025現在)なプラグインが一つのレポジトリに集っている最早スナックではなく、ご馳走プラグインです。folkewareらしい統一感のある、リッチなUIが提供されています。folkeさんの馬力には驚かされます。
そういえば、私の好きなプラグインの中にも似たようなコンセプトのプラグインがあります。それは、「echasnovski/mini.nvim」(以降、mini.nvim)です。こちらももはやminiではなく、maximumプラグインなのですが、snacks.nvimと違う点もあります。それは、シンプルなUIが提供されていることと、mini.hogeという名前でレポジトリが別れていて、個別でインストールできるという点です。

snacks.nvimが出た時に思いました。これを組み合わせたら初心者パックを組めるんじゃないか?と。

ということで今回は、snacks.nvimとmini.nvimを組み合わせた初心者パックを紹介します。

上記プラグインに加えて、以下4つのプラグインを導入します。

記事の対象

初心者パックを謳うのであれば、詳細に説明するのが当然かと思います。時間が足りず説明が不足しています。その分ソースコードにたくさんのコメントを書いているので、そこから読み取ってください。

公開後の追記

流石に文字だけだと分かりにくいのでgifを急いで用意します

追記おわり

全体ソース

この構成でフロントエンド開発をしてみましたが、問題なく使えそうでした。
https://gist.github.com/staticWagomU/41dc7dc75343874f1e78445144041331

コピペして使ってみてください。

ソースコード
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
vim.loader.enable()

-- mini.nvimのセットアップ
local path_package = vim.fn.stdpath('data') .. '/site'
local mini_path = path_package .. '/pack/deps/start/mini.nvim'
if not vim.loop.fs_stat(mini_path) then
	vim.cmd('echo "Installing `mini.nvim`" | redraw')
	local clone_cmd = {
		'git', 'clone', '--filter=blob:none',
		'https://github.com/echasnovski/mini.nvim', mini_path
	}
	vim.fn.system(clone_cmd)
	vim.cmd('packadd mini.nvim | helptags ALL')
end

-- mini.depsのセットアップ
require('mini.deps').setup({ path = { package = path_package } })

local add = MiniDeps.add
local now, later = MiniDeps.now, MiniDeps.later

vim.diagnostic.config({
	-- signcolumnに表示されるシンボルを設定
	signs = {
		text = {
			[vim.diagnostic.severity.ERROR] = "🚒",
			[vim.diagnostic.severity.WARN] = "🚧",
			[vim.diagnostic.severity.INFO] = "👀",
			[vim.diagnostic.severity.HINT] = "🦒",
		},
	},
	severity_sort = true,
	-- 末尾に表示されるvirtual_textを非表示
	virtual_text = false,
	-- 最近追加されたbuiltinのvirtual_linesを表示
	virtual_lines = {
		only_current_line = true,
		format = function(diagnostic)
			return string.format('%s (%s: %s)', diagnostic.message, diagnostic.source, diagnostic.code)
		end,

	}
})

-- clipboardの設定
vim.opt.clipboard = 'unnamedplus,unnamed'
-- インデントの設定
vim.opt.shiftwidth = 2
vim.opt.softtabstop = 2
vim.opt.tabstop = 2
-- statuslineの設定
vim.opt.laststatus = 3
vim.opt.cmdheight = 0

-- 行番号の設定
vim.opt.number = false

now(function()
	-- シンタックスハイライトに使用
	add {
		source = 'nvim-treesitter/nvim-treesitter',
		checkout = 'master',
		monitor = 'main',
		hooks = {
			post_checkout = function()
				vim.cmd('TSUpdate')
			end,
		},
	}

	---@diagnostic disable-next-line: missing-fields
	require('nvim-treesitter.configs').setup {
		auto_install = true,
		ensure_installed = { 'lua', 'vimdoc' },
		highlight = { enable = true },
		sync_install = true,
	}
end)

now(function()
	require('mini.notify').setup()
	-- 標準のnotifyを上書き
	vim.notify = require('mini.notify').make_notify()
end)

now(function()
	-- 画面下部に表示されるステータスラインの設定
	require('mini.statusline').setup()
end)

now(function()
	-- 色々な追加設定
	require('mini.extra').setup()
end)

now(function()
	-- mini.nvimが良い感じに初期設定してくれる
	require('mini.basics').setup()
end)

later(function()
	-- [  ]始まりのキーマッピングを設定
	-- bufferのトグルなど
	require('mini.bracketed').setup()
end)

later(function()
	-- signcolumnや行番号列にgitの差分に基づいて色が付く
	require('mini.diff').setup()
end)

later(function()
	-- :Git statusなどのコマンドを実行できる
	require('mini.git').setup()
end)

now(function()
	-- 󰈤 などのアイコンをいい感じに設定してくれる
	-- nvim_web_deviconsを入れなくてもmini.iconsで賄うことができる
	require('mini.icons').setup()
	MiniIcons.mock_nvim_web_devicons()
	MiniDeps.later(MiniIcons.tweak_lsp_kind)
end)

now(function()
	-- (等を入力するとペアになる)等が自動的に入力される
	-- 括弧忘れなどがなくなって便利
	require('mini.pairs').setup()
end)

now(function()
	-- 便利関数が入っている
	require('mini.misc').setup()
	-- ファイルを開き直したときに以前いたカーソル位置に戻ってくれる
	MiniMisc.setup_restore_cursor({
		center = false,
	})

	-- split等で画面分割しているときに:Zoomコマンドを実行すると
	-- フォーカスしているバッファが全面に表示される
	-- 再度、:Zoomコマンドを実行すると元に戻る
	-- いい機能
	vim.api.nvim_create_user_command('Zoom', function()
		MiniMisc.zoom(0, {})
	end, { desc = '' })
end)

now(function()
	-- ダブルクオーテーションをシングルクオートに書き換えたり
	-- 消したり、追加したりできる
	-- これに慣れるとvimから離れられなくなります
	require('mini.surround').setup()
end)

later(function()
	-- カーソル位置にある単語がハイライトされる
	-- 時々役に立つ
	require('mini.cursorword').setup { delay = 200 }
end)

later(function()
	-- インデントが分かりやすくなる
	require('mini.indentscope').setup { delay = 200 }
end)

later(function()
	-- tabにアイコンが付いたりと分かりやすくなる
	require('mini.tabline').setup()
end)

later(function()
	require('mini.files').setup()
	vim.keymap.set('n', '<Leader>E', function()
		MiniFiles.open(vim.api.nvim_buf_get_name(0))
	end)
	vim.api.nvim_create_autocmd("User", {
		pattern = "MiniFilesActionRename",
		callback = function(event)
			Snacks.rename.on_rename_file(event.data.from, event.data.to)
		end,
	})
end)

now(function()
	-- テキストオブジェクトを作成できる
	local ai = require('mini.ai')
	ai.setup({
		custom_textobjects = {
			-- viBとすることでバッファ全体を選択できたり
			B = MiniExtra.gen_ai_spec.buffer(),
			I = MiniExtra.gen_ai_spec.indent(),
			-- yiLで行コピーができる
			L = MiniExtra.gen_ai_spec.line(),
			-- treesitterと連携させることでさらに色々なことができる
			F = ai.gen_spec.treesitter({ a = '@function.outer', i = '@function.inner' }),
			i = ai.gen_spec.treesitter({ a = '@conditional.outer', i = '@conditional.inner' }),
		},
	})
end)

later(function()
	-- mini.nvim版 which-key
	local miniclue = require('mini.clue')
	miniclue.setup({
		clues = {
			miniclue.gen_clues.builtin_completion(),
			miniclue.gen_clues.g(),
			miniclue.gen_clues.marks(),
			miniclue.gen_clues.registers(),
			miniclue.gen_clues.windows({ submode_resize = true }),
			miniclue.gen_clues.z(),
		},
		triggers = {
			{ mode = 'n', keys = '<Leader>' }, -- Leader triggers
			{ mode = 'x', keys = '<Leader>' },
			{ mode = 'n', keys = [[\]] },   -- mini.basics
			{ mode = 'n', keys = '[' },     -- mini.bracketed
			{ mode = 'n', keys = ']' },
			{ mode = 'x', keys = '[' },
			{ mode = 'x', keys = ']' },
			{ mode = 'n', keys = 'g' }, -- `g` key
			{ mode = 'x', keys = 'g' },
			{ mode = 'i', keys = '<C-r>' },
			{ mode = 'c', keys = '<C-r>' },
			{ mode = 'n', keys = '<C-w>' }, -- Window commands
			{ mode = 'n', keys = 'z' },  -- `z` key
			{ mode = 'x', keys = 'z' },
			{ mode = 'n', keys = '<leader>l' },
		},
		window = { config = { border = 'single' } },
	})
end)

now(function()
	add('https://github.com/folke/snacks.nvim')
	require('snacks').setup({
		indent = {
			enabled = true,
			indent = { enabled = true },
			scope = { enabled = false },
			animate = { enabled = false },
		},
		statuscolumn = { enabled = true },
		picker = {
			layout = { preset = 'ivy' },
		},
		bigfile = { enabled = true },
	})

	-- ファイルピッカーを開く
	vim.keymap.set('n', '<Leader>e', Snacks.picker.files, { desc = 'open file' })
	-- バッファーを全部削除する
	vim.keymap.set('n', '<Leader>bd', Snacks.bufdelete.delete, { desc = 'delete buffer' })
	-- ターミナルをトグルする
	vim.keymap.set('n', '<Leader><Leader>', Snacks.terminal.toggle, { desc = 'toggle terminal' })
	vim.keymap.set('t', '<Leader><Leader>', Snacks.terminal.toggle, { desc = 'toggle terminal' })
end)



later(function()
	-- 入力補完ウィンドウを出してくれる
	require('mini.completion').setup {
		window = {
			info = { border = 'single' },
			signature = { border = 'single' },
		},
		lsp_completion = {
			source_func = 'completefunc',
			auto_setup = true,
			process_items = function(items, base)
				-- Don't show 'Text' and 'Snippet' suggestions
				items = vim.tbl_filter(function(x)
					return x.kind ~= 1 and x.kind ~= 15
				end, items)
				return MiniCompletion.default_process_items(items, base)
			end,
		},
	}
	if vim.fn.has('nvim-0.11') == 1 then
		---@diagnostic disable-next-line: undefined-field
		vim.opt.completeopt:append('fuzzy') -- Use fuzzy matching for built-in completion
	end
end)

later(function()
	add {
		source = 'neovim/nvim-lspconfig',
		depends = {
			'williamboman/mason.nvim',
			'williamboman/mason-lspconfig.nvim',
		},
	}
	require('mason').setup()
	require('mason-lspconfig').setup({
		ensure_installed = {
			'astro',
			'lua_ls',
			'ts_ls',
		},
		automatic_installation = true,
	})

	local lspconfig = require('lspconfig')
	local capabilities = vim.lsp.protocol.make_client_capabilities()

	require('mason-lspconfig').setup_handlers {
		function(server_name)
			lspconfig[server_name].setup {
				capabilities = capabilities,
			}
		end,
		['lua_ls'] = function()
			lspconfig['lua_ls'].setup {
				capabilities = capabilities,
				settings = {
					Lua = {
						runtime = {
							version = 'LuaJIT',
							pathStrict = true,
							path = { '?.lua', '?/init.lua' },
						},
						completion = { callSnippet = 'Both' },
						diagnostics = { globals = { 'vim' } },
						telemetry = { enable = false },
						workspace = {
							library = vim.list_extend(vim.api.nvim_get_runtime_file('lua', true), {
								'${3rd}/luv/library',
								'${3rd}/busted/library',
								'${3rd}/luassert/library',
								vim.api.nvim_get_runtime_file('', true),
							}),
							checkThirdParty = 'Disable',
						},
					},
				},
			}
		end,
	}

	vim.keymap.set('n', 'K', '<Cmd>lua vim.lsp.buf.hover()<Cr>', { desc = 'Hover' })
	vim.keymap.set('n', '<Leader>lf', '<Cmd>lua vim.lsp.buf.format()<Cr>', { desc = 'format code' })
	vim.keymap.set('n', '<Leader>lr', Snacks.picker.lsp_references, { desc = 'references' })
	vim.keymap.set('n', '<Leader>ld', Snacks.picker.lsp_definitions, { desc = 'go to definition' })
	vim.keymap.set('n', '<Leader>lD', Snacks.picker.lsp_declarations, { desc = 'go to declaration' })
	vim.keymap.set('n', '<Leader>li', Snacks.picker.lsp_implementations, { desc = 'go to implementation' })
	vim.keymap.set('n', '<Leader>lt', Snacks.picker.lsp_type_definitions, { desc = 'go to type definition' })
	vim.keymap.set('n', '<Leader>ln', '<Cmd>lua vim.lsp.buf.rename()<Cr>', { desc = 'rename symbol' })
	vim.keymap.set('n', '<Leader>lc', '<Cmd>lua vim.lsp.buf.code_action()<Cr>', { desc = 'code action' })
	vim.keymap.set('n', '<Leader>lI', '<Cmd>lua vim.lsp.buf.incoming_calls()<Cr>', { desc = 'incoming calls' })
	vim.keymap.set('n', '<Leader>lo', '<Cmd>lua vim.lsp.buf.outgoing_calls()<Cr>', { desc = 'outgoing calls' })
	vim.keymap.set('n', '<Leader>le', '<Cmd>lua vim.diagnostic.open_float()<Cr>', { desc = 'open diagnostics' })
	vim.keymap.set('n', ']l', '<Cmd>lua vim.diagnostic.goto_next()<Cr>', { desc = 'go to next diagnostics' })
	vim.keymap.set('n', '[l', '<Cmd>lua vim.diagnostic.goto_prev()<Cr>', { desc = 'go to previous diagnostics' })
end)

vim.cmd.colorscheme('minicyan')

解説

輪ごむのお気に入りポイントは、一部のlsp関連の操作をsnacks.nvimのpickerに委ねているところです。

1234567891011121314
	vim.keymap.set('n', 'K', '<Cmd>lua vim.lsp.buf.hover()<Cr>', { desc = 'Hover' })
	vim.keymap.set('n', '<Leader>lf', '<Cmd>lua vim.lsp.buf.format()<Cr>', { desc = 'format code' })
	vim.keymap.set('n', '<Leader>lr', Snacks.picker.lsp_references, { desc = 'references' })
	vim.keymap.set('n', '<Leader>ld', Snacks.picker.lsp_definitions, { desc = 'go to definition' })
	vim.keymap.set('n', '<Leader>lD', Snacks.picker.lsp_declarations, { desc = 'go to declaration' })
	vim.keymap.set('n', '<Leader>li', Snacks.picker.lsp_implementations, { desc = 'go to implementation' })
	vim.keymap.set('n', '<Leader>lt', Snacks.picker.lsp_type_definitions, { desc = 'go to type definition' })
	vim.keymap.set('n', '<Leader>ln', '<Cmd>lua vim.lsp.buf.rename()<Cr>', { desc = 'rename symbol' })
	vim.keymap.set('n', '<Leader>lc', '<Cmd>lua vim.lsp.buf.code_action()<Cr>', { desc = 'code action' })
	vim.keymap.set('n', '<Leader>lI', '<Cmd>lua vim.lsp.buf.incoming_calls()<Cr>', { desc = 'incoming calls' })
	vim.keymap.set('n', '<Leader>lo', '<Cmd>lua vim.lsp.buf.outgoing_calls()<Cr>', { desc = 'outgoing calls' })
	vim.keymap.set('n', '<Leader>le', '<Cmd>lua vim.diagnostic.open_float()<Cr>', { desc = 'open diagnostics' })
	vim.keymap.set('n', ']l', '<Cmd>lua vim.diagnostic.goto_next()<Cr>', { desc = 'go to next diagnostics' })
	vim.keymap.set('n', '[l', '<Cmd>lua vim.diagnostic.goto_prev()<Cr>', { desc = 'go to previous diagnostics' })

おわりに

うん、いい感じです。