Dark Powerに染め上げろ!NixVim×dpp.vim
Changelog
2025-03-17: 記事の内容を一部修正しました。
こんにちは。VimといえばDark Power、Dark Powerといえば中学2年生のときに一度は誰しもが憧れその身に宿そうとしたものを計算機で再現することに成功した黒魔術の一種であり、Shougowareの名で知られていることは周知の事実ではありますが、Vimを取り巻く黒魔術はShougowareだけではありません。昨今の黒魔術界隈ではすでに一般構築魔法[1]──Nix──が台頭しており、Neovimに特化した上級魔法であるNixVimも世間に浸透してきています。今回は、NixVimとdpp.vimを併用したNeovimのプラグイン管理について話します。
モチベーション
NixVimは、Nixの再現性と宣言的な性質の恩恵を受けつつプラグインの管理が可能なNeovim distributionです。NixVimが対応しているプラグインとnixpkgsのvimPlugins
配下のプラグインをNix式で管理することができます。nixpkgsに実装されていないプラグインについても、Neovimがそのまま利用できる形式のプラグインであればpkgs.vimUtils.buildVimPlugin
を使って自身でビルドすることが可能です。
{pkgs, lib, inputs, ...}: let
my-plugin = (pkgs.vimUtils.buildVimPlugin {
name = "my-plugin";
src = pkgs.fetchFromGitHub {
owner = "<owner>";
repo = "<repo>";
rev = "<commit hash>";
hash = "<nix NAR hash>";
};
})
in
{
programs.nixvim = {
enable = true;
colorschemes.catppuccin.enable = true;
plugins.lualine.enable = true;
extraPlugins = [
pkgs.vimPlugins.quick-scope
my-plugin
];
};
}
ここではmy-plugin
をビルドしてextraPlugins
に追加していますが、GitHubのリポジトリからプラグインを取得するにはpkgs.fetchFromGitHub
にrev
とhash
を指定する必要があり、これらは事前にnix-prefetch-git
コマンドで取得しておきます。これはNixの再現性を保持するために必要な手順ですが、nixpkgsの管轄外のプラグインを取得/更新するときこのような手順を毎回踏むのは少々面倒です。そこで、VimのプラグインマネージャをNixVimと併用することを考えます。
dpp.vim
さて、一言にプラグインマネージャといっても、多種多様なものが存在します。選定基準はなんでしょうか?NixVimは宣言的にNeovimとプラグインの管理ができ、基本的には記述したNix式のみが評価されます。勝手にバックグラウンドでよしなにやってくれるプラグインマネージャを導入すれば、「宣言的」でなくなってしまうかもしれません。また、近頃のプラグインマネージャならば遅延読み込みなどについて独自の最適化がなされているはずですが、NixVimとの併用ではその恩恵を十分に受けられるとはいえないでしょう。これらの条件を踏まえて、dpp.vimを採用します。
dpp.vimの設計思想や詳しい機能の解説については他の方の記事に譲りますが、簡単に特徴を説明します:
- 設定なしではなにも実行しない
- プラグインごとの遅延読み込みの設定からインストールされる場所まですべてを使用者が設定可能
- プラグインマネージャとしての機能は拡張機能をプラグインとしてインストールすることで使用
- denops.vimに依存しており、設定をTypeScriptで記述
dpp.vimが依存しているdenops.vimについてはnixpkgsに実装されています。
NixVimでdpp.vimをインストールする
本記事の目的はnixpkgsの管轄外のプラグインをdpp.vimで管理することですが、dpp.vimとその拡張機能自体はpkgs.vimUtils.buildVimPlugin
を使ってインストールします。rev
とhash
については記事執筆時点のものをそのまま載せているため、コピペでfetchが可能です。extraConfigLuaPost
にはdpp.vimのセットアップを行うLuaスクリプトを書いています。
{pkgs, ...}: let
dpp-vim = pkgs.vimUtils.buildVimPlugin {
name = "dpp.vim";
src = pkgs.fetchFromGitHub {
owner = "Shougo";
repo = "dpp.vim";
rev = "188f2852326d2e962f9afbf92d5bcb395ca2cb56";
hash = "sha256-UsKiSu0wtC0vdb7DZfvfrbqeHVXx5OPS/L2f/iABIWw=";
};
};
dpp-ext-installer = pkgs.vimUtils.buildVimPlugin {
name = "dpp-ext-installer";
src = pkgs.fetchFromGitHub {
owner = "Shougo";
repo = "dpp-ext-installer";
rev = "af4c066a9d9c8ba6938810556184fdec413063f1";
hash = "sha256-8jY5k/zEIXcIfqsMVfQXUvApRnJWavV4UmD9TCwMGv8=";
};
};
dpp-ext-lazy = pkgs.vimUtils.buildVimPlugin {
name = "dpp-ext-lazy";
src = pkgs.fetchFromGitHub {
owner = "Shougo";
repo = "dpp-ext-lazy";
rev = "839e74094865bdb2a548f1f43ab2752243182d31";
hash = "sha256-Izgv61SLT096WaPauWFdIKgXZWomGSC9NinciAQEIx4=";
};
};
dpp-ext-toml = pkgs.vimUtils.buildVimPlugin {
name = "dpp-ext-toml";
src = pkgs.fetchFromGitHub {
owner = "Shougo";
repo = "dpp-ext-toml";
rev = "b6e4b8dbe27fb8fab838c8898c8d329dceb7b759";
hash = "sha256-0qtL8tY4v3Vk/7cJahhg0+tLF6EM+U8A9R8OjzWSUyY=";
};
};
dpp-protocol-git = pkgs.vimUtils.buildVimPlugin {
name = "dpp-protocol-git";
src = pkgs.fetchFromGitHub {
owner = "Shougo";
repo = "dpp-protocol-git";
rev = "a5f8e67c1eefb009e7067f74d0615597e91a6c86";
hash = "sha256-BZeO5uedLeyCAPD1SvXk/nPIjTn1LuIAlGQAu4u65Qk=";
};
};
in
{
# dpp.vimが依存するためインストール
extraPlugins = [
pkgs.vimPlugins.denops-vim;
];
extraConfigLuaPre = ''
-- dpp.vimと拡張機能をruntimepathに追加
vim.opt.runtimepath:prepend("${dpp-vim}")
vim.opt.runtimepath:prepend("${dpp-ext-installer}")
vim.opt.runtimepath:prepend("${dpp-ext-lazy}")
vim.opt.runtimepath:prepend("${dpp-ext-toml}")
vim.opt.runtimepath:prepend("${dpp-protocol-git}")
local dpp = require("dpp")
local dpp_base = "<dpp.vimが使用するpath>"
local dpp_config = "<dpp.tsへのpath>"
if dpp.load_state(dpp_base) then
vim.api.nvim_create_autocmd("User", {
pattern = "DenopsReady",
callback = function ()
vim.notify("vim load_state is failed")
dpp.make_state(dpp_base, dpp_config)
end
})
end
vim.api.nvim_create_autocmd("User", {
pattern = "Dpp:makeStatePost",
callback = function ()
vim.notify("dpp make_state() is done")
end
})
'';
}
extraConfigLuaPre
のdpp_base
にはキャッシュやプラグインのインストールに使うディレクトリを、dpp_config
にはdpp.vimの設定ファイルへのpathを指定します。前者については事前にディレクトリをつくっておくか、home.activation
などで宣言的にするとよいでしょう。後者に関しては、筆者はhome-managerで~/.config/dppに設置しています。
このdpp.nixを、NixVimモジュールのエントリーポイントからimports
に追加して読み込みます。
{pkgs, ...}: {
programs.nixvim = {
enable = true;
...
imports = [
...
./plugins/dpp.nix
];
};
}
これでNixVimでのdpp.vimのインストールと、Luaスクリプトを用いたdpp.vimのセットアップが完了しました。dpp.ts
と各プラグイン用のtomlファイルは既存のもののままで動きますが、denops.vimとdpp.vimと拡張機能はdpp.vim自体で管理しないため、関連する部分をコメントアウトするか、dpp.tsで読み込まないように各自で調整してください。
ここまでの構成の問題点
上記の構成で、すでに以下のことができるようになっています:
- NixVimによる宣言的なプラグインの管理
- dpp.vimによるnixpkgsにないプラグインの管理
しかし、今のままでは唯一達成できないことがあります。それは、dpp.vimと拡張機能自体のアップデートです。
もちろん、リポジトリの更新を確認してrev
とhash
を更新すればアップデートは可能ですが、そのような手間を避けるためにdpp.vimを導入した経緯があるので、これでは本末転倒です。この問題を解決するためには、fetchFromGitHub
の代わりにNix flakesを使います。
flake.nixのinputsにdpp.vimを追加する
ここからは任意の操作になります。flake.nixのinputsにdpp.vimと拡張機能を追加します。
{
inputs = {
nixpkgs = ...
dpp-vim = {
url = "github:Shougo/dpp.vim";
flake = false;
};
dpp-ext-installer = {
url = "github:Shougo/dpp-ext-installer";
flake = false;
};
dpp-ext-lazy = {
url = "github:Shougo/dpp-ext-lazy";
flake = false;
};
dpp-ext-toml = {
url = "github:Shougo/dpp-ext-toml";
flake = false;
};
dpp-protocol-git = {
url = "github:Shougo/dpp-protocol-git";
flake = false;
};
};
outputs = ...
}
次にinputs
をdpp.nixに渡します。imports内の形式を変更しましょう。
{pkgs, lib, inputs, ...}: {
programs.nixvim = {
enable = true;
...
imports = [
...
- ./plugins/dpp.nix
+ # 必要な引数を渡す
+ (import ./plugins/dpp.nix {inherit pkgs lib inputs;})
];
};
}
これでflakeのinputs
をdpp.nixに渡すことができたので、pkgs.vimUtils.buildVimPlugin
のsrc = pkgs.fetchFromGitHub ...
をそれぞれsrc = inputs.dpp-*
に置き換えればいいのですが、同じ作業を5回やる必要もないので、mapなどを使って少し楽をします。
- {pkgs, ...}: let
- dpp-vim = pkgs.vimUtils.buildVimPlugin {
- # 省略
- };
- dpp-ext-installer = pkgs.vimUtils.buildVimPlugin {
- # 省略
- };
- dpp-ext-lazy = pkgs.vimUtils.buildVimPlugin {
- # 省略
- };
- dpp-ext-toml = pkgs.vimUtils.buildVimPlugin {
- # 省略
- };
- dpp-protocol-git = pkgs.vimUtils.buildVimPlugin {
- # 省略
- };
+ {pkgs, lib, inputs, ...}: let
+ # inputsからdpp.vimと拡張機能を抽出してプラグインとしてビルドする
+ dpp-plugins =
+ lib.attrsets.mapAttrsToList
+ (name: src: pkgs.vimUtils.buildVimPlugin {inherit name src;}) (lib.attrsets.getAttrs
+ [
+ "dpp-vim"
+ "dpp-ext-installer"
+ "dpp-ext-lazy"
+ "dpp-ext-toml"
+ "dpp-protocol-git"
+ ]
+ inputs);
+ # ビルドしたプラグインをruntimepathに追加
+ dpp-rtp-config =
+ lib.strings.concatMapStrings (plugin: ''
+ vim.opt.runtimepath:prepend("${plugin}")
+ '')
+ dpp-plugins;
in {
# dpp.vimが依存するためインストール
extraPlugins = [
pkgs.vimPlugins.denops-vim;
];
extraConfigLuaPre = ''
-- dpp.vimと拡張機能をruntimepathに追加
- vim.opt.runtimepath:prepend("${dpp-vim}")
- vim.opt.runtimepath:prepend("${dpp-ext-installer}")
- vim.opt.runtimepath:prepend("${dpp-ext-lazy}")
- vim.opt.runtimepath:prepend("${dpp-ext-toml}")
- vim.opt.runtimepath:prepend("${dpp-protocol-git}")
+ ${dpp-rtp-config}
local dpp = require("dpp")
local dpp_base = "<dpp.vimが使用するpath>"
local dpp_config = "<dpp.tsへのpath>"
if dpp.load_state(dpp_base) then
vim.api.nvim_create_autocmd("User", {
pattern = "DenopsReady",
callback = function()
vim.notify("vim load_state is failed")
dpp.make_state(dpp_base, dpp_config)
end,
})
end
vim.api.nvim_create_autocmd("User", {
pattern = "Dpp:makeStatePost",
callback = function()
vim.notify("dpp make_state() is done")
end,
})
'';
}
これでdpp.vimと拡張機能をnix flake update
でアップデートできるようになり、NixVimとdpp.vimという2つのDark Powerを使いこなすことができました。
おわりに
NixVimとdpp.vimを併用したプラグインの管理を行ってみました。declarative・reproducibleである点が魅力的なNixですが、その分即座に変更することが難しいのは否めません。ローカルにあるプラグインや、最新のアップデートをNixのビルドプロセスを待たずに利用したい場合もあるでしょう。その点、dpp.vimは使用者がやることをすべて自身で設定し把握する、所謂fully configurableなプラグインマネージャです。的確に欠点を補っており、相性も抜群といえます。NixVim×dpp.vimに移行して数日経ちましたが、これといった不具合もなく使用できており、パフォーマンスも申し分ありません[2]。
このアイディアは筆者のdotfilesで実際に試したものなので、行き詰まったときにはぜひ覗いてみてください。
-
一般構築魔法については(この記事)[https://zenn.dev/natsukium/articles/b4899d7b1e6a9a]が詳しい ↩︎
-
移行前(dpp.vim)は30ms前後だった起動時間が、移行後には遅延読み込みの設定を一切していない状態で7ms程度になった(!?) ↩︎
Discussion