Skip to main content
Deno 2 is finally here 🎉️
Learn more

dvpm - Denops Vim/Neovim Plugin Manager !

DeepWiki

dvpm is a plugin manager for Vim and Neovim, powered by denops.vim.

  • Vim / Neovim start up very fast!

…but plugins are not loaded yet at startup \(^o^)/

All plugins are loaded lazily.

  • You can write all Vim / Neovim settings in TypeScript

Requirement

Sample configuration

Neovim

  • ~/.config/nvim/init.lua (Mac / Linux)
  • ~/AppData/Local/nvim/init.lua (Windows)
local denops = vim.fn.expand("~/.cache/nvim/dvpm/github.com/vim-denops/denops.vim")
if not vim.loop.fs_stat(denops) then
  vim.fn.system({ "git", "clone", "https://github.com/vim-denops/denops.vim", denops })
end
vim.opt.runtimepath:prepend(denops)

Vim

  • ~/.vimrc (Mac / Linux)
  • ~/_vimrc (Windows)
let s:denops = expand("~/.cache/vim/dvpm/github.com/vim-denops/denops.vim")
if !isdirectory(s:denops)
  execute 'silent! !git clone https://github.com/vim-denops/denops.vim ' .. s:denops
endif
execute 'set runtimepath^=' . substitute(fnamemodify(s:denops, ':p') , '[/\\]$', '', '')

deno.json

If you use denops.vim v8 or later with imports in deno.json, you must specify workspace.

{
  "workspace": [
    "./denops/config"
  ]
}

And add dependencies in ./denops/config.

cd ./denops/config
deno add jsr:@denops/std jsr:@yukimemi/dvpm

Neovim

  • ~/.config/nvim/denops/config/main.ts (Mac / Linux)
  • ~/AppData/Local/nvim/denops/config/main.ts (Windows)

Vim

  • ~/.vim/denops/config/main.ts (Mac / Linux)
  • ~/vimfiles/denops/config/main.ts (Windows)
import type { Denops, Entrypoint } from "@denops/std";
import * as fn from "@denops/std/function";
import * as mapping from "@denops/std/mapping";
import * as vars from "@denops/std/variable";
import { execute } from "@denops/std/helper";

import { Dvpm } from "@yukimemi/dvpm";

export const main: Entrypoint = async (denops: Denops) => {
  const base_path = (await fn.has(denops, "nvim")) ? "~/.cache/nvim/dvpm" : "~/.cache/vim/dvpm";
  const base = (await fn.expand(denops, base_path)) as string;

  // First, call Dvpm.begin with denops object and base path.
  const dvpm = await Dvpm.begin(denops, { base });

  // URL only (GitHub).
  await dvpm.add({ url: "yukimemi/autocursor.vim" });
  // URL only (not GitHub).
  await dvpm.add({ url: "https://notgithub.com/some/other/plugin" });
  // With branch.
  // await dvpm.add({ url: "neoclide/coc.nvim", rev: "release" });

  // hook_add (dein.vim) equivalent.
  // Execute at startup regardless of whether the plugin is lazy-loaded.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    lazy: true,
    add: async ({ denops }) => {
      // Set global variables before plugin is loaded.
      await vars.g.set(denops, "hitori_debug", 1);
    },
  });

  // build option. Execute after install or update.
  await dvpm.add({
    url: "neoclide/coc.nvim",
    rev: "master",
    build: async ({ info }) => {
      if (!info.isUpdate || !info.isLoad) {
        // build option is called after git pull, even if there are no changes
        // so you need to check for changes
        return;
      }
      const args = ["install", "--frozen-lockfile"];
      const cmd = new Deno.Command("yarn", { args, cwd: info.dst });
      const output = await cmd.output();
      console.log(new TextDecoder().decode(output.stdout));
    },
  });
  // shallow clone.
  await dvpm.add({ url: "yukimemi/chronicle.vim", depth: 1 });
  // Setting before sourcing.
  await dvpm.add({
    url: "yukimemi/silentsaver.vim",
    before: async ({ denops }) => {
      await vars.g.set(
        denops,
        "silentsaver_dir",
        (await fn.expand(denops, "~/.cache/nvim/silentsaver")) as string,
      );
    },
  });
  // Setting after sourcing.
  await dvpm.add({
    url: "folke/which-key.nvim",
    after: async ({ denops }) => {
      await execute(denops, `lua require("which-key").setup()`);
    },
  });
  // dst setting (for development).
  await dvpm.add({
    url: "yukimemi/lumiris.vim",
    dst: "~/src/github.com/yukimemi/lumiris.vim",
    before: async ({ denops }) => {
      await mapping.map(denops, "<space>ro", "<cmd>ChangeColorscheme<cr>", {
        mode: "n",
      });
      await mapping.map(
        denops,
        "<space>rd",
        "<cmd>DisableThisColorscheme<cr>",
        { mode: "n" },
      );
      await mapping.map(denops, "<space>rl", "<cmd>LikeThisColorscheme<cr>", {
        mode: "n",
      });
      await mapping.map(denops, "<space>rh", "<cmd>HateThisColorscheme<cr>", {
        mode: "n",
      });
    },
  });
  // Disable setting.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    enabled: false,
  });
  // Disable with function.
  await dvpm.add({
    url: "editorconfig/editorconfig-vim",
    enabled: async ({ denops }) => !(await fn.has(denops, "nvim")),
  });
  // With dependencies. dependencies plugin must be added.
  await dvpm.add({ url: "lambdalisue/askpass.vim" });
  await dvpm.add({ url: "lambdalisue/guise.vim" });
  await dvpm.add({
    url: "lambdalisue/gin.vim",
    dependencies: [
      "lambdalisue/askpass.vim",
      "lambdalisue/guise.vim",
    ],
  });
  // Load from file. ( `.lua` or `.vim` )
  await dvpm.add({
    url: "rcarriga/nvim-notify",
    beforeFile: "~/.config/nvim/rc/before/nvim-notify.lua",
    afterFile: "~/.config/nvim/rc/after/nvim-notify.lua",
  });

  // Finally, call Dvpm.end.
  await dvpm.end();

  console.log("Load completed !");
};

See my dotfiles for more complex examples.

dotfiles/.config/nvim at main · yukimemi/dotfiles · GitHub

API

Dvpm.begin

public static async begin(denops: Denops, dvpmOption: DvpmOption): Promise<Dvpm>
export type DvpmOption = {
  // Base path for git clone.
  base: string;
  // Cache file path. See `Cache setting`.
  cache?: string;
  // If specified in profiles, only plugins that match the profiles specified in `Plug` will be loaded
  // See `Profile setting`
  profiles?: string[];
  // Number of concurrent processes. Default is 8.
  // This is used plugin install, update, source.
  concurrency?: number;
  // Use `vim.notify` for Install and Update log. Default is false. (Neovim only)
  notify?: boolean;
  // git log arg. Used for :DvpmUpdate command output. Default is [].
  logarg?: string[];
};

Dvpm.end

public async end(): Promise<void>

Add plugins to runtimepath and source plugin/*.vim and plugin/*.lua.

Dvpm.add

public async add(plug: Plug): Promise<void>
export type Plug = {
  // GitHub `username/repository` or URL that can be cloned with git.
  url: string;
  // Plugin name. If omitted, it's calculated from the URL or dst. (Optional)
  name?: string;
  // The path to git clone. (Optional)
  dst?: string;
  // Git branch or revision name. (Optional)
  rev?: string;
  // clone depth. (Optional)
  depth?: number;
  // Enable or disable. Default is true.
  enabled?: Bool;
  /**
   * List of profiles this plugin belongs to.
   */
  profiles?: string[];
  /**
   * Configuration to run at startup. (Regardless of lazy)
   */
  add?: ({ denops, info }: { denops: Denops; info: PlugInfo }) => Promise<void>;
  /**
   * Configuration to run before adding to runtimepath.
   */
  before?: ({ denops, info }: { denops: Denops; info: PlugInfo }) => Promise<void>;
  /**
   * Configuration to run after adding to runtimepath.
   */
  after?: ({ denops, info }: { denops: Denops; info: PlugInfo }) => Promise<void>;
  /**
   * Path to a Vim/Lua file to source at startup. (Regardless of lazy)
   */
  addFile?: string;
  /**
   * Path to a Vim/Lua file to source before adding to runtimepath.
   */
  beforeFile?: string;
  /**
   * Path to a Vim/Lua file to source after adding to runtimepath.
   */
  afterFile?: string;
  /**
   * Build configuration to run after installation or update.
   */
  // Cache settings. See `Cache setting`.
  cache?: {
    enabled?: Bool;
    before?: string;
    after?: string;
    beforeFile?: string;
    afterFile?: string;
  };
  // Whether to git clone and update. Default is true. (Optional)
  // If this option is set to false, then `enabled` is also set to false.
  clone?: Bool;
  // Dependencies. (Optional)
  dependencies?: string[];
  // Lazy load. (Optional)
  lazy?: boolean;
  // Load the plugin when the command is executed. (Optional)
  cmd?: string | string[];
  // Load the plugin when the event is triggered. (Optional)
  event?: string | string[];
  // Load the plugin when the filetype is detected. (Optional)
  ft?: string | string[];
  // Load the plugin when the key is pressed. (Optional)
  keys?: string | string[];
};
export type Bool =
  | boolean
  | (({
    denops,
    info,
  }: {
    denops: Denops;
    info: PlugInfo;
  }) => Promise<boolean>);

PlugInfo type is almost same as Plug. Contains the calculated or defined results for each variable, such as enabled and name.

Dvpm.cache

public async cache(arg: { script: string; path: string }): Promise<void>

Cache the script to path.

e.g.

await dvpm.cache({
  script: `
    if !v:vim_did_enter && has('reltime')
      let s:startuptime = reltime()
      au VimEnter * ++once let s:startuptime = reltime(s:startuptime) | redraw
            \\ | echomsg 'startuptime: ' .. reltimestr(s:startuptime)
    endif
  `,
  path: "~/.config/nvim/plugin/dvpm_cache.vim",
});

await dvpm.cache({
  script: `
    vim.g.loaded_2html_plugin = 1
    vim.g.loaded_gzip = 1
    vim.g.loaded_man = 1
    vim.g.loaded_matchit = 1
    vim.g.loaded_matchparen = 1
    vim.g.loaded_netrwPlugin = 1
    vim.g.loaded_tarPlugin = 1
    vim.g.loaded_tutor_mode_plugin = 1
    vim.g.loaded_zipPlugin = 1
  `,
  path: "~/.config/nvim/plugin/dvpm_cache.lua",
});

Dvpm.list

public list(): Plugin[]

If you want a list of plugin information, you can get it with the dvpm.list() function. The return value is Plugin[]. See the doc for type information.

Command

:DvpmUpdate [url]

Update installed plugins.

If url is specified, update only target plugins, if not specified, update all plugins.

:DvpmList

It outputs the list of plugins to the dvpm://list buffer.

Cache setting

If you want some plugins to be loaded before VimEnter, enable the cache setting. A sample configuration is shown below.

export const main: Entrypoint = async (denops: Denops) => {
  const base_path = (await fn.has(denops, "nvim")) ? "~/.cache/nvim/dvpm" : "~/.cache/vim/dvpm";
  const base = (await fn.expand(denops, base_path)) as string;
  const cache_path = (await fn.has(denops, "nvim"))
    ? "~/.config/nvim/plugin/dvpm_plugin_cache.vim"
    : "~/.config/vim/plugin/dvpm_plugin_cache.vim";
  // This cache path must be prepended to the runtimepath.
  // Add it in your vimrc or init.lua yourself, or specify a path already included in
  // the runtimepath of Vim / Neovim.
  const cache = (await fn.expand(denops, cache_path)) as string;

  // Specify `cache` to Dvpm.begin.
  const dvpm = await Dvpm.begin(denops, { base, cache });

  await dvpm.add({
    url: "tani/vim-artemis",
    // Just set `cache.enabled` to true if you don't need plugin settings.
    cache: { enabled: true },
  });
  await dvpm.add({
    url: "nvim-lua/plenary.nvim",
    cache: { enabled: true },
    enabled: async ({ denops }) => await fn.has(denops, "nvim"),
  });

  await dvpm.add({
    url: "startup-nvim/startup.nvim",
    // deno-lint-ignore require-await
    enabled: async ({ denops }) => denops.meta.host === "nvim",
    // Specify `before` or `after` if you need to configure the plugin.
    // `before` is executed before the plugin is added to the runtimepath.
    // `after` is executed after the plugin is added to the runtimepath.
    cache: {
      before: `echomsg "Load startup !"`,
      after: `
        lua require("startup").setup({ theme = "startify" })
      `,
    },
  });

  await dvpm.add({
    url: "rcarriga/nvim-notify",
    enabled: async ({ denops }) => await fn.has(denops, "nvim"),
    cache: {
      // `before` and `after` can be set independently.
      after: `
        lua << EOB
          require("notify").setup({
            stages = "slide",
          })
          vim.notify = require("notify")
        EOB
      `,
      // If you want to read from a separate file, specify it as follows. (.lua and .vim can be used)
      // afterFile: "~/.config/nvim/rc/after/notify.lua",
    },
  });

  // Finally, call Dvpm.end.
  await dvpm.end();
};

After configuring the above settings, starting Vim / Neovim will output the following to the file specified as cache in Dvpm.begin. The next time Vim / Neovim starts, the plugin will be enabled before VimEnter.

  • ~/.config/nvim/plugin/dvpm_plugin_cache.vim (for Neovim)
" This file is generated by dvpm.
set runtimepath+=/Users/yukimemi/.cache/nvim/dvpm/github.com/tani/vim-artemis
set runtimepath+=/Users/yukimemi/.cache/nvim/dvpm/github.com/nvim-lua/plenary.nvim
echomsg "Load startup !"
set runtimepath+=/Users/yukimemi/.cache/nvim/dvpm/github.com/startup-nvim/startup.nvim
lua require("startup").setup({theme = "startify"})
set runtimepath+=/Users/yukimemi/.cache/nvim/dvpm/github.com/rcarriga/nvim-notify
lua << EOB
require("notify").setup({
stages = "slide",
})
vim.notify = require("notify")
EOB

Lazy Loading

You can use lazy, cmd, event, ft, and keys to load plugins lazily.

e.g.

  // Lazy load.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    lazy: true,
  });

  // Load on command.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    cmd: "Hitori",
  });

  // Load on event.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    event: "BufRead",
  });

  // Load on filetype.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    ft: "typescript",
  });

  // Load on keys.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    keys: "<leader>h",
  });

  // Load on keys with object (lazy.nvim equivalent).
  // It will NOT unmap after loading, but remap to `rhs`.
  await dvpm.add({
    url: "yukimemi/hitori.vim",
    keys: { lhs: "<leader>h", rhs: "<cmd>Hitori<cr>", mode: "n" },
  });

Hook execution order

  1. add / addFile: Always executed at startup (Dvpm.end()).
  2. before / beforeFile: Executed just before adding to runtimepath. (Delayed if lazy)
  3. after / afterFile: Executed just after adding to runtimepath and sourcing plugin/*.vim. (Delayed if lazy)
  4. build: Executed after install or update.

Autocmd

  • DvpmCacheUpdated

Fires after updating the cache.

  • Dvpm:PreLoad:{pluginName}

Fires before loading a plugin (after before and beforeFile processing). {pluginName} is the name property of PlugInfo.

  • Dvpm:PostLoad:{pluginName}

Fires after loading a plugin (after after, afterFile, and build processing). {pluginName} is the name property of PlugInfo.

e.g.

import * as autocmd from "@denops/std/autocmd";

~~~

await autocmd.define(denops, "User", "DvpmCacheUpdated", "echo 'dvpm cache updated !'");

// Use wildcard to hook all plugins
await autocmd.define(
  denops,
  "User",
  "Dvpm:PostLoad:*",
  `echom "Loaded plugin: " . substitute(expand("<amatch>"), "^Dvpm:PostLoad:", "", "")`,
);

Profile setting

If profiles is specified in DvpmOption, the plugins to be enabled can be restricted by the specified profile.

e.g.

~~~
export const main: Entrypoint = async (denops: Denops) => {
  const base_path = (await fn.has(denops, "nvim")) ? "~/.cache/nvim/dvpm" : "~/.cache/vim/dvpm";
  const base = (await fn.expand(denops, base_path)) as string;

  const dvpm = await Dvpm.begin(denops, {
    base,
    // Use only minimal plugins
    profiles: ["minimal"],
  });

  await dvpm.add({
    url: "yukimemi/chronicle.vim",
    profiles: ["minimal"],
  });
  await dvpm.add({
    url: "yukimemi/silentsaver.vim",
    profiles: ["default"],
  });
  await dvpm.add({
    url: "yukimemi/autocursor.vim",
    profiles: ["full"],
  });

  await dvpm.end();
};

In this case, only yukimemi/chronicle.vim is enabled.

e.g.

~~~
export const main: Entrypoint = async (denops: Denops) => {
  const base_path = (await fn.has(denops, "nvim")) ? "~/.cache/nvim/dvpm" : "~/.cache/vim/dvpm";
  const base = (await fn.expand(denops, base_path)) as string;

  const dvpm = await Dvpm.begin(denops, {
    base,
    // Use only minimal and default plugins
    profiles: ["minimal", "default"],
  });

  await dvpm.add({
    url: "yukimemi/chronicle.vim",
    profiles: ["minimal"],
  });
  await dvpm.add({
    url: "yukimemi/silentsaver.vim",
    profiles: ["default"],
  });
  await dvpm.add({
    url: "yukimemi/autocursor.vim",
    profiles: ["full"],
  });

  await dvpm.end();
};

In this case, yukimemi/chronicle.vim and yukimemi/silentsaver.vim are enabled.

If you specify ["minimal", "default", "full"] in DvpmOption.profiles, all three plugins will be enabled.

Debug logging

dvpm uses @std/log for logging. If you want to see debug logs, you need to setup the logger.

e.g.

import { setup, handlers } from "@std/log";

setup({
  handlers: {
    console: new handlers.ConsoleHandler("DEBUG"),
  },
  loggers: {
    dvpm: {
      level: "DEBUG",
      handlers: ["console"],
    },
  },
});