🌘

Dark Powerに染め上げろ!NixVim×dpp.vim

に公開
Changelog

2025-03-17: 記事の内容を一部修正しました。

こんにちは。VimといえばDark PowerDark Powerといえば中学2年生のときに一度は誰しもが憧れその身に宿そうとしたものを計算機で再現することに成功した黒魔術の一種であり、Shougowareの名で知られていることは周知の事実ではありますが、Vimを取り巻く黒魔術Shougowareだけではありません。昨今の黒魔術界隈ではすでに一般構築魔法[1]──Nix──が台頭しており、Neovimに特化した上級魔法であるNixVimも世間に浸透してきています。今回は、NixVimdpp.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.fetchFromGitHubrevhashを指定する必要があり、これらは事前に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を使ってインストールします。revhashについては記事執筆時点のものをそのまま載せているため、コピペでfetchが可能です。extraConfigLuaPostにはdpp.vimのセットアップを行うLuaスクリプトを書いています。

plugins/dpp.nix
{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
      })
    '';
  }

extraConfigLuaPredpp_baseにはキャッシュやプラグインのインストールに使うディレクトリを、dpp_configにはdpp.vimの設定ファイルへのpathを指定します。前者については事前にディレクトリをつくっておくか、home.activationなどで宣言的にするとよいでしょう。後者に関しては、筆者はhome-managerで~/.config/dppに設置しています。
このdpp.nixを、NixVimモジュールのエントリーポイントからimportsに追加して読み込みます。

default.nix
{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と拡張機能自体のアップデートです。
もちろん、リポジトリの更新を確認してrevhashを更新すればアップデートは可能ですが、そのような手間を避けるためにdpp.vimを導入した経緯があるので、これでは本末転倒です。この問題を解決するためには、fetchFromGitHubの代わりにNix flakesを使います。

flake.nixのinputsにdpp.vimを追加する

ここからは任意の操作になります。flake.nixのinputsにdpp.vimと拡張機能を追加します。

flake.nix
{
  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内の形式を変更しましょう。

default.nix
{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.buildVimPluginsrc = pkgs.fetchFromGitHub ...をそれぞれsrc = inputs.dpp-*に置き換えればいいのですが、同じ作業を5回やる必要もないので、mapなどを使って少し楽をします。

plugins/dpp.nix
-  {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を使いこなすことができました。

おわりに

NixVimdpp.vimを併用したプラグインの管理を行ってみました。declarative・reproducibleである点が魅力的なNixですが、その分即座に変更することが難しいのは否めません。ローカルにあるプラグインや、最新のアップデートをNixのビルドプロセスを待たずに利用したい場合もあるでしょう。その点、dpp.vimは使用者がやることをすべて自身で設定し把握する、所謂fully configurableなプラグインマネージャです。的確に欠点を補っており、相性も抜群といえます。NixVim×dpp.vimに移行して数日経ちましたが、これといった不具合もなく使用できており、パフォーマンスも申し分ありません[2]
このアイディアは筆者のdotfilesで実際に試したものなので、行き詰まったときにはぜひ覗いてみてください。
https://github.com/glassesneo/dotfiles

脚注
  1. 一般構築魔法については(この記事)[https://zenn.dev/natsukium/articles/b4899d7b1e6a9a]が詳しい ↩︎

  2. 移行前(dpp.vim)は30ms前後だった起動時間が、移行後には遅延読み込みの設定を一切していない状態で7ms程度になった(!?) ↩︎

Discussion