🖤

プラグインマネージャーの歴史と新世代のプラグインマネージャー dpp.vim

2023/10/13に公開

始めに

ddc.vim, ddu.vim の開発が一通り終了し、次に作成するプラグインについて考えていました。
バイナリ編集プラグイン ddx.vim の開発を進めることも考えていたのですが、dein.vim の開発から長い時間がたっており新たなプラグインマネージャーも出てきているので、そろそろ作り直すべきではないかと考えました。
プラグインマネージャーの機能がどんどん複雑化する昨今、プラグインマネージャーに必要な機能とは何か考えたときに、「拡張可能なプラグインマネージャーが求められている」と思ったのです。

そして今回作成したプラグインマネージャーは dpp.vim です。

https://github.com/Shougo/dpp.vim

dpp.vim は作成したばかりで完成度はまだ低く、dein.vim ユーザーが乗り換える機能のレベルや安定性にはなっていません。
しかし、その設計思想とインタフェースは十分固まったといえるので今回記事を書くことにしました。

プラグインマネージャーの歴史

私は Vim キャリアが長いので、これまでいくつかのプラグインマネージャーを使ってきましたし、開発してきました。
ここではプラグインマネージャーの歴史を振り返ってみることにしましょう。

プラグインマネージャーを使わなかった時代 (2008 年以前)

Vim プラグインが本格的に作られるようになったのはスクリプト機能が強化された Vim 7.0 がリリースされた 2006 年頃です。
私が Vim を触り始めたのも丁度この時期でした。

その当時、プラグインマネージャーというものは存在しませんでした。
現在では考えられないと思うのですが、~/.vim にプラグインを手で放り込むということが行われていたのです。
当時のプラグインは plugin ディレクトリに .vim ファイルが一つしかないシンプルな構成が多く、プラグインの数も少なかったのでそれで済んだのでした。

vim-pathogen (2008/10 頃)

https://github.com/tpope/vim-pathogen

2008 年になると vim-pathogen というプラグインマネージャーが使われるようになりました。
vim-pathogen のプラグイン管理スタイルは従来の管理のように特定のディレクトリに全てを放り込むのではなく、プラグインをサブディレクトリに分けます。
それぞれのサブディレクトリにあるプラグインを全て読み込む仕組みとなっています。
現代でいうと node_modulespip の管理方法に近いでしょうか。それぞれのプラグインのディレクトリは独立しており、別のプラグインのファイルが混ざらないので管理がしやすくなっています。

当時 github が出たばかりであったため、vim-pathogen は git, github 前提のプラグインマネージャーとまではいえないでしょう。
しかしプラグインが大きくなり数も多くなると手で管理できなくなったので vim-pathogen が登場したのは想像に難くありません。
特定ディレクトリにプラグインを git clone すれば比較的簡単にプラグインをインストールできるのは魅力といえます。

vim-pathogen は原始的なプラグインマネージャーです。
名前から分かるようにプラグインのロードパスの管理だけを目的としており、それ以外のことはほとんど何もしません。

vim-pathogen は Easy ではありませんが Simple であり、理解がしやすく拡張も容易です。私は vim-pathogen をベースにしているプラグインマネージャーが幾つもあったことも知っています。
後にこの思想は Vim の packages 機能として本体に取り込まれます。

Vundle.vim (2010/10 頃)

https://github.com/VundleVim/Vundle.vim

2010 年から 2011 年にかけて、一世を風靡したのは Vundle.vim というプラグインマネージャーでした。

Vundle.vim が広く使われるようになったのは、「プラグインマネージャーにインストール機能を統合した」ためでしょう。
これは明らかに git, github を前提とした仕組みです。面倒なインストール作業を git, github 前提にすることで簡単にしたため、爆発的に流行しました。

Vundle.vim によりプラグインインストールが簡単になったのはその時代背景が大きく関係しています。
この頃にはプラグインを大量にインストールすることが増えてきており、数十ものプラグインをインストールすることも普通となりました。
この量を手で git clone するのは現実的ではありません。インストールの自動化は必須となりました。
インストールを自動化しておくと、他の環境でも同じプラグインセットを使い始めることが簡単にできるため、そういう意味でも便利な存在でした。

Vundle.vim において特徴的な機能は {name}/{repo} という記法でプラグインをインストールできるということです。これは github を Vim プラグインの標準リポジトリとしてみなしているわけです。

neobundle.vim (2011/09 頃)

https://github.com/Shougo/neobundle.vim

neobundle.vim は当時広く使われていた Vundle.vim をベースに私が最初に開発したプラグインマネージャーです。

neobundle.vim は遅延起動にこだわって設計されました。乱暴に表現すると、Vundle.vim に遅延起動を追加したのが neobundle.vim です。
なぜ neobundle.vim はプラグインの遅延起動を導入したのでしょうか。
これは当時プラグインのインストール数が爆発的に増えたことに関係しています。
Vim はプラグインを起動時に全てロードしてしまうのでプラグインが増えると Vim の起動時間が増えてしまうのです。
かといって、ユーザーは起動時間を削減するために使うプラグインを削減したくないでしょう。
そこでプラグインマネージャーに遅延起動という概念が導入されました。

プラグインの遅延起動とは、プラグインをプラグインを使うときにだけロードするように設定することです。
Vim 標準である autoload に似た仕組みですが、プラグイン単位でロード管理する違いがあります。
プラグインの遅延起動をうまく設定することで、プラグインを読み込まない Vim と遜色ない起動時間にすることが可能です。
もちろんプラグインの遅延起動には設定の複雑化というトレードオフがあります。遅延起動を正しく設定するにはユーザーの深い知識が必要で簡単に設定できるものではありません。
プラグインのロード時間のチューニング方法の一つと考えてもらえば結構です。

vim-plug (2013/09 頃)

https://github.com/junegunn/vim-plug

Vundle.vim の開発が停滞していたころ、2013 年 9 月に現れたのは vim-plug でした。
vim-plug が独自に行ったこととして大きいのはプラグインの大量並列インストール(プラグインインストールの高速化)、インストール UI の改良でしょう。
この変更がなぜ行われたのか想像してみるに、Vundle.vim の時代よりも更にプラグインのインストールが増えてきたということがあると思います。
もう当時はプラグインを 100 個 200 個導入しているという状態は普通となっていました。
プラグインを沢山インストールしていると、当然プラグインのアップデート時間はかかるわけです。インストーラーの並列化は必須であったでしょう。

vim-plug の面白い思想としてはプラグインマネージャーをシンプル化し、1 ファイルで配布することでインストールを容易にしようというものが挙げられます。
1 ファイルで配布されている場合、次のようにファイルをダウンロードするコマンド一つでプラグインマネージャーを導入することができるわけです。

$ curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

vim-plug が開発されてから既に 10 年ほど経ち開発が落ちついていますが、今だにファンが多いプラグインマネージャーです。

dein.vim (2015/12 頃)

https://github.com/Shougo/dein.vim

neobundle.vim を長年開発した結果、機能と実装が複雑化し開発の継続が困難になってきました。一度機能の整理をしようと思いフルスクラッチで開発したのが dein.vim となります。
dein.vim の特徴としては設定をユーザーに委ねること、UI はシンプルであること、ユーザーが設定をチューニングできるようにプラグインマネージャーが余計なことをしないことです。

neobundle.vim では遅延起動によりパフォーマンスをチューニングするとができましたが、dein.vim ではより細かいプラグインマネージャーの挙動をチューニングできるようにしています。
dein.vimneobundle.vim よりも更に機能が多いのでちゃんと設定するのは難しいです。適切な設定さえできればプラグインマネージャー最速クラスの速度を叩き出すことが可能です。

私は現在でも dein.vim を利用していますし、機能面での不満はないです。

Vim packages 機能(2016/09 頃正式リリース)

:help packages

Vim 8.0 の目玉機能の一つとして、Vim にパッケージ管理機能(packages)が追加されました。

ちなみに、packages 機能はプラグインパスを管理するだけでありプラグインをインストールする方法は存在していません。
プラグインインストールは本体に入れるには複雑であり、プラグインをインストールする標準リポジトリも存在しないためです。

要するに vim-pathogen 相当の機能が Vim 本体に組込まれたわけです。本体はあくまでバックエンドを提供し、プラグインマネージャーのフロントエンドは自由に選択できるようになったとも言えます。

packages 機能はプラグインの読み込みについて癖があります。これは本体で実装することを考えると仕方のない部分です。
例えば vimrc の読み込みのあとに 'runtimepath' が設定されるため、vimrc 内で autoload 関数を読み込めません。

https://zenn.dev/kuu/articles/vim-pack-attention

minpac (2017/02 頃)

https://github.com/k-takata/minpac

Vim 本体への packages 機能の実装により、packages を利用した幾つものプラグインマネージャーが開発されていきます。

minpacpackages 機能を用いてシンプルに実装されたプラグインマネージャーです。
Vim の packages 機能を使いやすくしたフロントエンドともいえます。
packages 機能が好きならよい選択肢だと思います。

packer.nvim (2017/11 頃)

https://github.com/wbthomason/packer.nvim

packer.nvimvim-plug を Lua で再実装することを目指したプラグインマネージャーです。
Lua で書かれていることから当然 neovim 専用となります。

packer.nvim は Lua で書かれた neovim プラグインと相性がよいのが特徴です。neovim の事実上標準プラグインマネージャーとして愛用者が多くいました。
ただし minpac と同じく packer.nvim の実装には Vim packages 機能を用いており、プラグインの読み込みに独特の癖があるため注意が必要です。

vim-jetpack (2022/02 頃)

https://github.com/tani/vim-jetpack

vim-jetpack の特徴は vim-plug と同じく 1 ファイルのみでインストール可能であること、簡単に使えることにあります。
プラグインマネージャーとしては珍しく、'runtimepath' のマージ機能に対応しています。

vim-jetpackdein.vim と比較するとチューニングできるところが少ない代わりに初心者には分かりやすいと思います。

lazy.nvim (2022/11 頃)

https://github.com/folke/lazy.nvim

最近登場した新進気鋭のプラグインマネージャーです。開発が停滞している packer.nvim の後継として彗星のように現れました。
その特徴は UI のスタイリッシュさと起動時間のチューニングにあると思います。名前の通り neovim 専用なのは Vim ユーザーにとっては残念といえるでしょうか。

lazy.nvim はある意味でプラグインマネージャーの一つの到達点であり、今現在最も開発が盛んなプラグインマネージャーといってもよいでしょう。

dpp.vim (2023/09 頃)

https://github.com/Shougo/dpp.vim

そして私が今回開発したのが dpp.vim です。
dpp.vim はプラグインマネージャーとしては最後発にあたり、他のプラグインマネージャーにはない思想で設計されています。
その特徴的な機能については次のセクションにて詳しく説明します。

私は dpp.vim を作るにあたって、ユーザーがコードを書けば何でもできるということをコンセプトにしました。
既存のプラグインマネージャーはなんでもやってくれる代わりに、ユーザーにコードを書かせないようになっています。
これはいけません。プラグインマネージャーはユーザーの要望を叶えてしかるべきです。ユーザーは自分の要望のためならコードも書くでしょう。
コードを書かない人は dpp.vim のユーザーではありません。
プラグインマネージャーを使うのはテキストエディタの素人ではなく、コードを書けるプログラマーなのですから。

dpp.vim の特徴

dpp.vim の特徴は以下の通りです。

  • ほとんど全ての機能をコアから分離する

  • プラグインのインストール機能は拡張機能として提供

  • TypeScript と Vim script のハイブリッド実装

  • 変数の廃止、オプションの動的変更は不可

  • Deno に依存する代わりに環境依存は最小限

ほとんど全ての機能をコアから分離する

私が dpp.vim で目指したのは機能のモジュール化と分離です。

dpp.vimdein.vim の備えるほとんどの機能を拡張機能として実装しています。
例えば以下のようなものが挙げられます。

  • プラグインのインストール(dpp-ext-installer)

https://github.com/Shougo/dpp-ext-installer

  • toml サポート(dpp-ext-toml)

https://github.com/Shougo/dpp-ext-toml

  • ローカルプラグインの読み込み(dpp-ext-local)

https://github.com/Shougo/dpp-ext-local

  • git サポート(dpp-protocol-git)

https://github.com/Shougo/dpp-protocol-git

dpp.vim はデフォルトでは何も動作しないことを目指します。vim-plug, vim-jetpack の「簡単にインストールでき、すぐに使い始められるプラグインマネージャー」とは真逆に位置する思想となります。
dpp.vim の思想は easy ではなく究極の simple なのです。easy なプラグインマネージャーが必要な人には既に沢山の選択肢があるので問題にはなりません。

もちろん、この決断によってプラグインマネージャーのインストールは少し大変になるかもしれません。
しかし、プラグインマネージャーのインストールはそんなに頻繁に行うものでしょうか。インストール手順が複雑ならスクリプト化すればよいのではないでしょうか。
dpp.vim はテキストエディタの素人が使うことを意図していないし、プラグインマネージャーのインストールは簡単にする必要はありません。

プラグインマネージャーの歴史を見れば分かる通り、それは複雑化の歴史です。
この理由は分かっています。ユーザーにとって必要な機能が異なるので、それらの要望を全て叶えようとするとユーザーが増加するにあたって無限に機能が増えて複雑化していくのです。
そして最終的には開発が放棄され、次のプラグインマネージャーが開発されるという歴史を繰り返しています。
もちろん、プラグインマネージャーの一部にはシンプルさを売りにしたものも存在しています。しかしユーザーによって必要な機能が異なるので、機能を単純に削ぎ落とすことには問題があります。

dpp.vim ではコア機能とそうでない機能が分離されていることにより、ユーザーは自分に必要な機能のみをインストールすればよくなります。ユーザーは足りない機能を自分で作ってもよいし、自由自在です。
もちろん、プラグインマネージャーをインストールする前に考えることは増えています。しかしこれこそが楽しいのではないでしょうか。あなたは自由を手にいれたのです。

dpp.vim では dpp.vim と拡張機能のみで既存のプラグインマネージャーの機能を再現することが可能です。

例えば、

  • dpp.vim + dpp-ext-local: pathogen.vim 相当

  • dpp.vim + dpp-ext-installer: Vundle.vim 相当

  • dpp.vim + dpp-ext-installer + dpp-ext-lazy: vim-plug 相当

  • dpp.vim + dpp-ext-installer + dpp-ext-lazy + dpp-ext-local + dpp-ext-toml: dein.vim 相当

拡張機能をインストールするだけでシンプルなプラグインマネージャーとしても、複雑なプラグインマネージャーとしても振る舞うことのできる dpp.vim はある意味で最強のプラグインマネージャーと呼べるでしょう。

みなさんは既存のプラグインマネージャーに不満があったりしませんか。
自分に不必要な機能がプラグインマネージャーに含まれていることが気になりませんか。
一部の挙動を修正するためにプラグインマネージャーを fork するなどしていませんか。
おめでとうございます。その問題は dpp.vim で解決できます。dpp.vim では既存の機能や拡張機能に不満があればあなた自身で拡張を作ってしまえばよいのです。

「ユーザーはコードを書かないというこれまでの常識」から解き放たれ、プラグインを設定するという自由を手に入れましょう。

プラグインのインストール機能は拡張機能として提供

dpp.vim の機能分離の一環として、インストール機能を分離することにしました。
現代ではプラグインマネージャーにインストール機能は必須のように考えられています。
しかし太古のプラグインマネージャーである pathogen.vim や Vim のパッケージ管理機能がそうであるように、プラグインのインストール機能というのはプラグインマネージャーにとって必須ではありません。
更に、プラグインのインストール機能というのは実装が複雑になりがちです。

dpp.vim ではインストール機能を dpp-ext-installer として分離することにより、コアの機能をシンプルにします。

例えば、dein.vim では以下のようにプラグインをインストールしていました。

call dein#install()

同じことを dpp.vim では以下のように行います。

call dpp#async_ext_action('installer', 'install')

dein.vim の場合は専用の API を用意してプラグインのインストール作業を行います。

dpp.vim の場合は専用の API というものはなく、あくまでも汎用的な拡張機能の呼び出し(dpp#async_ext_action())により全ての処理を実装しています。
installer 拡張機能(dpp-ext-installer) の install アクションを呼び出すことでプラグインのインストール作業を行います。
私はこちらのほうが設計が美しいと思いました。あなたはどう思うでしょうか。

TypeScript と Vim script のハイブリッド実装

プラグインマネージャーの開発に用いる言語の選択はとても難しいものでした。
最初から実装に Lua や Vim 9 script を使うことは選択肢にありませんでした。
Lua だと neovim 専用になってしまうし、既存のプラグインマネージャーが既に存在しています。
Vim 9 script は neovim で動作しない上に未完成、今後の開発にも不安が残ります。

そこで白羽の矢をたてたのが denops.vim を用いた TypeScript によるプラグインマネージャーの実装でした。TypeScript によるプラグイン開発は ddc.vim, ddu.vim でも行っており実績も十分です。
TypeScript を用いると型情報により堅牢なプラグインが書けるし、機能のモジュール化やその読み込みも簡単です。
しかし一つの課題がありました。denops.vim は遅延起動するので Vim 起動時には使えません。
プラグインマネージャーはその性質上どうしても Vim 起動時に動作する必要があります。denops.vim のロード時間も課題となりました。

私はそこで発想の転換をすることにしました。
「TypeScript でプラグインマネージャーの機能全てを記述するのではなく、起動時に必要な最小限の機能を Vim script で実装する、複雑な処理のみを TypeScript 化する」と。

dein.vim ではそもそも state ファイルという概念が存在しています。
これは起動速度の高速化のために起動時キャッシュを生成し、次回の起動のときはそちらを読み込むことで限界まで高速化するという手法です。
これを応用し、「dpp.vim では起動時キャッシュを生成する部分を TypeScript 化する」という設計にまとまりました。
起動時やプラグイン読み込みで必要な部分は Vim script が残りますが、それでも随分と Vim script の量を減らせる計算です。

この決断により、ユーザーの設定もキャッシュ生成部分(TypeScript)とキャッシュ(Vim script)読み込み部分で分かれることとなりました。
Vim script を書く量が減るのはユーザーにも開発者にも歓迎される部分でしょう。

変数の廃止、オプションの動的変更は不可

dpp.vim では ddc.vimddu.vim と同じく設定変数を廃止しました。
設定変数を廃止することによりインタフェースは固定化され、暗黙的な設定はできなくなります。
更に、設定した値はキャッシュされて固定されるので動的に変更することはできません。これはユーザーに不便を強いる仕様ではありますが、一種の割り切りとなっています。

Deno に依存する代わりに環境依存は最小限

dein.vim では外部依存がほぼ無かったのにかかわらず、dpp.vim では Deno(denops.vim) に依存してしまっています。
これはマイナス面ばかりかというと、そうでもありません。

dein.vim には環境に依存しないという代わりにインストーラーのパフォーマンスが悪かったり、環境に依存したコードがありました。
dpp.vimDeno の機能をそのまま使えるので環境に依存したコードがほとんどありません。
Windows でも Mac でも Linux でも Vim でも neovim でも使用することが可能なのです。

dpp.vim の使用方法

dpp.vim は親切なプラグインマネージャーではありません。
詳しい使用方法はドキュメントを参照しながら試行錯誤してください、としたいのですが簡単に説明をしておきます。

dpp.vim には Vim script による設定部分と TypeScript による設定部分があります。
ユーザーは両者が何をしているのか理解する必要があります。

Vim script 設定部分について

const s:dpp_base = '~/.cache/dpp/'

" Set dpp source path (required)
const s:dpp_src = '~/.cache/dpp/repos/github.com/Shougo/dpp.vim'
const s:denops_src = '~/.cache/dpp/repos/github.com/vim-denops/denops.vim'

" Set dpp runtime path (required)
execute 'set runtimepath^=' .. s:dpp_src

if dpp#min#load_state(s:dpp_base)
  " NOTE: dpp#make_state() requires denops.vim
  execute 'set runtimepath^=' .. s:denops_src
  autocmd User DenopsReady
  \ call dpp#make_state(s:dpp_base, '{TypeScript config file path}')
endif

dpp.vimdenops.vim は既にインストールされているとします。
まず、dpp.vim の関数を読み込めるように dpp.vim に 'runtimepath' を通します。これはプラグインマネージャーを使うなら常識であると言えるでしょう。

dpp#min#load_state() がポイントです。これは dpp.vim の state を読み込む関数となります。
dpp#min#load_state() は state file が不正な場合や存在しない場合に失敗して if のブロックの中が実行されます。

if のブロック内では state file を作りなおすために dpp#make_state() を実行します。
dpp#make_state()denops.vim に依存しているので、'runtimepath' に denops.vim を追加しておかなければなりません。
dpp#make_state()denops.vim がロードされていなければ実行できないので、denops.vim のロード完了後(DenopsReady)に自動実行されるようにします。

dpp#min#load_state(), dpp#make_state() の引数に渡している s:dpp_base はプラグインのインストール先のベースとなるパスを表します。
dein.vim を知っているなら理解がしやすいでしょう。

dpp#make_state() における {TypeScript config file path} は次で説明するファイルへのパスのことです。
これはユーザーの TypeScript 設定ファイルを示します。

TypeScript 設定部分について

Vim script だけでは state file の作成と更新しかできないので、dpp.vim では TypeScript による設定が肝となります。
dein.vim とは異なり、Vim script による設定には対応していません。

TypeScript による設定ファイルは Config ファイルと呼びます。
これは ddc.vimddu.vim における load_config() に相当するものと覚えておくと分かりやすいでしょう。
TypeScript による設定は例えば以下のようなものです。

import {
  type ContextBuilder,
  type Plugin,
} from "jsr:@shougo/dpp-vim@~3.1.0/types";
import {
  BaseConfig,
  type ConfigReturn,
  type MultipleHook,
} from "jsr:@shougo/dpp-vim@~3.1.0/config";
import type {
  Ext as TomlExt,
  Params as TomlParams,
} from "jsr:@shougo/dpp-ext-toml@~1.3.0";
import type {
  Ext as LazyExt,
  LazyMakeStateResult,
  Params as LazyParams,
} from "jsr:@shougo/dpp-ext-lazy@~1.5.0";
import type {
  Ext as LocalExt,
  Params as LocalParams,
} from "jsr:@shougo/dpp-ext-local@~1.3.0";

import type { Denops } from "jsr:@denops/std@~7.3.0";
import * as fn from "jsr:@denops/std@~7.3.0/function";

export class Config extends BaseConfig {
  override async config(args: {
    denops: Denops;
    contextBuilder: ContextBuilder;
    basePath: string;
  }): Promise<ConfigReturn> {
    args.contextBuilder.setGlobal({
      protocols: ["git"],
    });

    const [context, options] = await args.contextBuilder.get(args.denops);

    const recordPlugins: Record<string, Plugin> = {};
    const ftplugins: Record<string, string> = {};
    const hooksFiles: string[] = [];
    let multipleHooks: MultipleHook[] = [];

    // Load toml plugins
    const [tomlExt, tomlOptions, tomlParams]: [
      TomlExt | undefined,
      ExtOptions,
      TomlParams,
    ] = await args.denops.dispatcher.getExt(
      "toml",
    ) as [TomlExt | undefined, ExtOptions, TomlParams];
    if (tomlExt) {
      const action = tomlExt.actions.load;

      const tomlPromises = [
        { path: "$BASE_DIR/dpp.toml", lazy: false },
      ].map((tomlFile) =>
        action.callback({
          denops: args.denops,
          context,
          options,
          protocols,
          extOptions: tomlOptions,
          extParams: tomlParams,
          actionParams: {
            path: tomlFile.path,
            options: {
              lazy: tomlFile.lazy,
            },
          },
        })
      );

      const tomls = await Promise.all(tomlPromises);

      // Merge toml results
      for (const toml of tomls) {
        for (const plugin of toml.plugins ?? []) {
          recordPlugins[plugin.name] = plugin;
        }

        if (toml.ftplugins) {
          mergeFtplugins(ftplugins, toml.ftplugins);
        }

        if (toml.multiple_hooks) {
          multipleHooks = multipleHooks.concat(toml.multiple_hooks);
        }

        if (toml.hooks_file) {
          hooksFiles.push(toml.hooks_file);
        }
      }
    }

    const [localExt, localOptions, localParams]: [
      LocalExt | undefined,
      ExtOptions,
      LocalParams,
    ] = await args.denops.dispatcher.getExt(
      "local",
    ) as [LocalExt | undefined, ExtOptions, LocalParams];
    if (localExt) {
      const action = localExt.actions.local;

      const localPlugins = await action.callback({
        denops: args.denops,
        context,
        options,
        protocols,
        extOptions: localOptions,
        extParams: localParams,
        actionParams: {
          directory: "~/work",
          options: {
            frozen: true,
            merged: false,
          },
        },
      });

      for (const plugin of localPlugins) {
        if (plugin.name in recordPlugins) {
          recordPlugins[plugin.name] = Object.assign(
            recordPlugins[plugin.name],
            plugin,
          );
        } else {
          recordPlugins[plugin.name] = plugin;
        }
      }
    }

    const [lazyExt, lazyOptions, lazyParams]: [
      LazyExt | undefined,
      ExtOptions,
      LazyParams,
    ] = await args.denops.dispatcher.getExt(
      "lazy",
    ) as [LazyExt | undefined, ExtOptions, PackspecParams];
    let lazyResult: LazyMakeStateResult | undefined = undefined;
    if (lazyExt) {
      const action = lazyExt.actions.makeState;

      lazyResult = await action.callback({
        denops: args.denops,
        context,
        options,
        protocols,
        extOptions: lazyOptions,
        extParams: lazyParams,
        actionParams: {
          plugins: Object.values(recordPlugins),
        },
      });
    }

    return {
      ftplugins,
      hooksFiles,
      multipleHooks,
      plugins: lazyResult?.plugins ?? [],
      stateLines: lazyResult?.stateLines ?? [],
    };
  }
}

TypeScript 部分では使用する protocol(protocols オプション) の設定をしたり、
拡張機能からプラグインリストの読み込みを行ったりします。

上記では dpp-ext-tomlload action を使って toml からプラグインリストを取得しています。
これは dein.vim における dein#load_toml() と同等です。

更に、dpp-ext-localload action を用いてローカルディレクトリにあるプラグインリストを取得することができます。

dpp-ext-toml, dpp-ext-local どちらもプラグインリストを生成するので、両者をマージしなければいけません。
実は dein.vim では内部で自動的にマージを行っていましたが、dpp.vim ではユーザーがコードを書いて自らの手でマージする必要があります。
自分でコードを書くことにより高度な柔軟性が得られ、暗黙的な処理がなくなるのですから、これはよいトレードオフだと思います。

dpp-ext-lazy による遅延起動を使うには state file に記述を追加する必要があるので、makeState action を明示的に呼び出さなければいけません。
dpp.vim は親切ではないので、自動的に処理が追加されません。あなたが設定した通りに動作します。

Config ファイルでは plugins によるプラグインリストと stateLines による初期化設定を与える必要があります。

GitHub sponsors について

今回の dpp.vim プラグインの開発は GitHub sponsors の皆さんの支援によって行われました。
何年もかけて開発してきたプラグインを作り直すのはとてもエネルギーの必要な作業です。
皆さんからの支援がなければ、到底実現できなかっただろうと思います。

https://zenn.dev/shougo/articles/github-sponsors

https://github.com/sponsors/Shougo/

GitHub sponsors での支援は確実に自分のプラグインの開発意欲の向上に役立っているので、プラグインのユーザーに広く支援をお願いします。

Discussion