Introduction

Denops Mascot

Denops (/ˈdiːnoʊps/, pronounced as dee-nops) is an ecosystem designed for developing plugins for Vim and Neovim using Deno (a TypeScript / JavaScript runtime).

Denops and Denops plugins (Vim plugins powered by Denops) offer the following features:

  • Installable as a Vim plugin:
    Denops follows the standard Vim plugin architecture. Users can install Denops itself and Denops plugins using a Vim plugin manager, just like any other Vim plugins.
  • Unified codebase for Vim and Neovim:
    Denops provides a unified API for both Vim and Neovim. You can write a plugin that functions on both Vim and Neovim with a single codebase.
  • No worries about dependency management:
    Deno includes a built-in dependency management system, allowing developers to write plugins with third-party libraries without concerns about dependency management.
  • Simple and efficient code:
    Deno utilizes the V8 engine, significantly faster than Vim script. You can write a plugin with straightforward code, without the need for complex optimizations solely for performance.
  • Risk-free execution:
    Denops plugins run in a separate process from Vim / Neovim. Even if a plugin freezes, Vim / Neovim remains unaffected.

Check out vim-denops GitHub Topics to discover Vim plugins using Denops.

Denops is primarily developed and maintained by the vim-denops organization (separated from the vim-jp organization). For questions, you can use GitHub Discussions (English), or visit the #tech-denops channel on Slack workspace for vim-jp (Japanese).

Installation

Prerequisites

Denops requires Deno to be installed on your system. Please refer to Deno's official manual for installation instructions.

After installing Deno, ensure that the deno command is accessible from your Vim.

:echo execpath("deno")

It should display the path to the deno command. If it prints an empty string, add the path to the deno command to your PATH environment variable.

tip

If you prefer not to modify the PATH environment variable, you can set the executable path to the g:denops#deno variable in your vimrc file like this:

let g:denops#deno = "/path/to/deno"

Installation

Denops itself is a Vim plugin and can be installed using a Vim plugin manager, similar to other Vim plugins.

vim-plug
Plug 'vim-denops/denops.vim'
Jetpack.vim
Jetpack 'vim-denops/denops.vim'
dein.vim
call dein#add('vim-denops/denops.vim')
minpac
call minpac#add('vim-denops/denops.vim')
lazy.nvim
require("lazy").setup({
  "vim-denops/denops.vim",
  -- ...
})

Health Check

Denops provides a health checker to confirm that Denops is installed correctly. You can check the health of Denops by running the :checkhealth command (Neovim) or :CheckHealth (Vim with vim-healthcheck).

==============================================================================
denops: health#denops#check

- Supported Deno version: `1.45.0`
- Detected Deno version: `1.45.4`
- OK Deno version check: passed
- Supported Neovim version: `0.10.0`
- Detected Neovim version: `0.10.0`
- OK Neovim version check: passed
- Denops status: `running`
- OK Denops status check: passed

Getting Started

Denops (/ˈdiːnoʊps/, pronounced dee-nops) is an ecosystem for Vim / Neovim that empowers developers to write plugins in TypeScript / JavaScript powered by Deno.

Let's start by creating a simple plugin to learn how to develop Denops plugins.

Create a Plugin

Create a directory named denops-getting-started in your home directory and a file named main.ts within it, under denops/denops-getting-started/:

$HOME
└── denops-getting-started
    └── denops
        └── denops-getting-started
            └── main.ts

Next, write the following TypeScript code in main.ts:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async hello() {
      await denops.cmd(`echo "Hello, Denops!"`);
    },
  };
};

Activate the Plugin

Add the following line to your Vim or Neovim configuration file (e.g., ~/.vimrc or ~/.config/nvim/init.vim):

set runtimepath+=~/denops-getting-started

Or Neovim Lua configuration file (e.g., ~/.config/nvim/init.lua):

vim.opt.runtimepath:append("~/denops-getting-started")

Try the Plugin

Restart Vim/Neovim and execute the following command:

:call denops#request('denops-getting-started', 'hello', [])

You should see "Hello, Denops!" displayed on the screen like:

Explanation of the Getting Started

In this section, we'll provide detailed information about the Getting Started. If you find it too detailed, feel free to skip this section and move on to the next chapter, especially if your goal is to start developing a Denops plugin promptly.

What is Denops?

Denops claims to be an ecosystem for developing Vim / Neovim (hereafter, when we refer to Vim without restriction, we also include Neovim) plugins using Deno, but, in reality, it is a Vim plugin with the following features:

  • Detection and registration of Denops plugins
  • Launching and connecting to Deno processes
  • Calling Deno process-side functions from Vim via RPC (Remote Procedure Call)
  • Calling Vim features from Deno process-side via RPC

By utilizing this plugin, you can control Vim from code written in TypeScript (Denops plugins).

note

RPC (Remote Procedure Call) is used, and while Vim uses a JSON-based custom specification, Neovim uses MessagePack-RPC (a slightly modified specification). However, Denops abstracts away these differences, so Denops plugin developers don't need to be aware of the RPC specification differences between Vim and Neovim.

What is a Vim Plugin?

When Vim starts, it searches for files named plugin/*.vim in directories specified in runtimepath. Additionally, if a function like foo#bar#hoge() is called, it searches for files named autoload/foo/bar.vim in the runtimepath and reads the file, calling the foo#bar#hoge() function defined within.

A Vim plugin is a set of predefined features provided to users, utilizing the functionality mentioned above. Typically, an entry point is defined in plugin/{plugin_name}.vim, and detailed features are implemented in autoload/{plugin_name}.vim or autoload/{plugin_name}/*.vim. For example, here is the directory structure for a Vim plugin named hello:

vim-hello
├── autoload
│    └── hello.vim # Defines the function `hello#hello()`
└── plugin
     └── hello.vim # Defines the `Hello` command

note

For more detailed information on creating Vim plugins, refer to :help write-plugin.

What is a Denops Plugin?

When Denops is installed, in addition to Vim plugins, files named denops/*/main.ts are also searched when Vim starts. If a corresponding file is found, Denops registers the parent directory name (foo in the case of denops/foo/main.ts) as the plugin name. Then, it imports the corresponding file as a TypeScript module and calls the function named main.

A Denops plugin, similar to a Vim plugin, provides a set of features written in TypeScript to users. Since Denops plugins typically include both TypeScript and Vim script code, the directory structure looks like an extension of the Vim plugin structure with an added denops directory. For example, here is the directory structure for a Denops plugin named hello:

denops-hello
├── autoload
│    └── hello.vim # Tasks better written in Vim script (may not exist)
├── denops
│    └── hello
│           └── main.ts # Entry point for the Denops plugin (mandatory)
└── plugin
     └── hello.vim # Entry point written in Vim script (optional)

In the Getting Started, we created a file named denops/denops-getting-started/main.ts and added its parent directory (denops-getting-started) to runtimepath. There were no autoload or plugin directories because we didn't provide an entry point that Vim could easily call.

Understanding the Code in Getting Started

In the Getting Started, we wrote the following code in the main.ts file:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async hello() {
      await denops.cmd(`echo "Hello, Denops!"`);
    },
  };
};

Let's break down this code step by step.

About Imports

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";

The first line imports the Entrypoint type from the @denops/std standard library. You can find detailed information about the library by checking the URL: https://jsr.io/@denops/std@7.0.0 (replace jsr: to https://jsr.io/). We fixed the version in the import URL, so it's recommended to check for details and update to the latest version URL.

Note that we use import type syntax, which is part of TypeScript's Type-Only Imports and Export. This syntax can be written as import { type Entrypoint } with the same meaning. Using import { Entrypoint } for a type-only import is also valid.

note

Denops plugins are dynamically imported, so there might be differences in Denops versions between development and usage. Therefore, to minimize differences between Denops versions, only type information is exposed. The implementation can be found in denops/@denops-private/denops.ts, but it is not publicly exposed for the reasons mentioned above.

This type information is provided by @denops/core, and @denops/std simply re-exports the type information from @denops/core. However, @denops/core is intended to be referenced only by denops.vim and @denops/std, so Denops plugin developers don't need to use it directly.

About Entry Point

export const main: Entrypoint = (denops) => {
  // Omitted...
};

The above code exports the main function. The main function is called by Denops, and it takes the Denops instance (denops) as an argument. Denops plugins use this denops to add user-defined APIs or call Vim's features.

About User-Defined APIs

denops.dispatcher = {
  async hello() {
    // Omitted...
  },
};

The code above adds a user-defined API named hello to denops.dispatcher. denops.dispatcher is defined as follows, and each method takes unknown types for both arguments and return values:

interface Dispatcher {
  [key: string]: (...args: unknown[]) => unknown;
}

By defining methods in denops.dispatcher, you can freely define APIs. Since the methods registered in denops.dispatcher are always called with await, you can make them asynchronous by returning a Promise.

The methods defined in denops.dispatcher can be called from Vim using the following functions:

FunctionDescription
denops#requestSynchronously calls a user-defined API and returns the result.
denops#request_asyncAsynchronously calls a user-defined API and passes the result to callbacks.
denops#notifyCalls a user-defined API without waiting for completion and discards the result.

At the end of the Getting Started, we used denops#request('denops-getting-started', 'hello', []) to call the user-defined API named hello in denops-getting-started plugin.

About Calling Vim's features

await denops.cmd(`echo "Hello, Denops!"`);

With the received denops, you can call Vim functions, execute Vim commands, or evaluate Vim expressions. In the example above, the hello API internally uses denops.cmd to execute the echo command in Vim. The denops object provides several methods:

MethodDescription
callCalls a Vim function and returns the result.
batchCalls multiple Vim functions in bulk and returns the results in bulk.
cmdExecutes a Vim command. If ctx is provided, it is expanded as local variables.
evalEvaluate a Vim expression and returns the result. If ctx is provided, it is expanded as local variables.
dispatchCalls a user-defined API of another Denops plugin and returns the result.

Although denops provides low-level interfaces, @denops/std combines these low-level interfaces to offer higher-level interfaces. Therefore, it's recommended to use @denops/std to call Vim's features in actual plugin development.

For example, use function module to call Vim's function instead of denops.call like:

import * as fn from "jsr:@denops/std@^7.0.0/function";

// Bad (result1 is `unknown`)
const result1 = await denops.call("expand", "%");

// Good (result2 is `string`)
const result2 = await fn.expand(denops, "%");

If developers use function module instead, they can benefit from features like auto-completion and type checking provided by LSP (Language Server Protocol).

Next Steps

In the next step, follow the tutorial to learn how to develop a minimum Denops plugin.

Tutorial: Hello World

In this chapter, we will create a minimal Denops plugin that greets the user. Most of the code resembles the previous Getting Started, so feel free to skip this chapter if you are already familiar with it.

note

The plugin we will create in this section can be found at https://github.com/vim-denops/denops-helloworld.vim

Creating a Minimal Vim Plugin

Let's start by crafting a minimal Vim plugin called denops-helloworld.

Create a directory named denops-helloworld in your home directory and a Vim script file as follows:

denops-helloworld
└── plugin
     └── denops-helloworld.vim

The content of the plugin/denops-helloworld.vim file is as follows:

if exists('g:loaded_denops_helloworld')
  finish
endif
let g:loaded_denops_helloworld = 1

command! DenopsHello echo 'Hello, Denops!'

The initial four lines (as shown below) serve as a guard, preventing the plugin from being loaded more than once. Using this guard is a common practice to ensure the plugin is loaded only once.

if exists('g:loaded_denops_helloworld')
  finish
endif
let g:loaded_denops_helloworld = 1

The final line (as displayed below) defines a command named DenopsHello that invokes the echo 'Hello, Denops!' command. Thus, when you execute :DenopsHello in Vim, it will display "Hello, Denops!".

command! DenopsHello echo 'Hello, Denops!'

Upon startup, Vim searches and loads files named plugin/*.vim in directories specified in runtimepath. To activate the plugin, add the following line to your Vim configuration file (e.g., ~/.vimrc or ~/.config/nvim/init.vim):

set runtimepath+=~/denops-helloworld

For Neovim's Lua configuration file (e.g., ~/.config/nvim/init.lua), use:

vim.opt.runtimepath:append("~/denops-helloworld")

Restart Vim and execute :DenopsHello to witness the message "Hello, Denops!".

Creating a Minimal Denops Plugin

When denops.vim is installed, it searches for files named denops/*/main.ts in addition to Vim plugins when Vim starts.

If a corresponding file is found, Denops registers the parent directory name (foo in the case of denops/foo/main.ts) as the plugin name. It then imports the corresponding file as a TypeScript module and calls the function named main.

note

Denops plugins typically include both TypeScript and Vim script code, so the directory structure looks like an extension of the Vim plugin structure with an added denops directory.

Let's add denops/denops-helloworld/main.ts to the denops-helloworld directory that we created in the previous section. The directory tree will be as follows:

denops-helloworld
├── denops
│    └── denops-helloworld
│           └── main.ts
└── plugin
     └── denops-helloworld.vim

Here is the content of the denops/denops-helloworld/main.ts file:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";

export const main: Entrypoint = (denops) => {
  console.log("Hello, Denops from TypeScript!");
};

warning

As shown above, developers can use console.log (or console.warn, console.error, etc.) for debug output. The content will be echoed to Vim. However, it is not recommended to use console.log in production code. Instead, use denops.cmd("echo '...'") or the echo function in the helper module of the @denops/std library.

Once you've created the file, restart Vim, and "Hello, Denops from TypeScript!" will be displayed on Vim startup.

Adding Denops API to the Plugin

In the previous section, we created a minimal Denops plugin. In this section, we will enhance the plugin by adding an API.

Open denops/denops-helloworld/main.ts and rewrite the content with the following code:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { assert, is } from "jsr:@core/unknownutil@^4.3.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    hello(name) {
      assert(name, is.String);
      return `Hello, ${name || "Denops"}!`;
    },
  };
};

The above code adds a new API hello to the plugin. The hello API takes a string name and returns a greeting message. See About User-Defined APIs in Explanation of the Getting started for details about User-Defined APIs.

note

While Vim script does not facilitate types, Denops uses unknown types on the interface between Vim and Denops. That's why we use unknownutil to ensure that the name is of type string in the above code.

Once you've updated the file, restart Vim, and execute the following command, you will see the message "Hello, Your name!".

:echo denops#request("denops-helloworld", "hello", ["Your name"])

As shown, users can call the Denops API via the denops#request() function.

Calling Vim Features from the Plugin

If you want to use a Vim feature from your Denops plugin, you can call it via the denops instance passed to the plugin's main function. You can rewrite main.ts as follows to register the DenopsHello as a Vim command:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { assert, is } from "jsr:@core/unknownutil@^4.3.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async init() {
      // This is just an example.
      // Developers usually should define commands directly in Vim script.
      await denops.cmd(
        `command! -nargs=? DenopsHello echomsg denops#request('denops-helloworld', 'hello', [<q-args>])`,
      );
    },

    hello(name) {
      assert(name, is.String);
      return `Hello, ${name || "Denops"}!`;
    },
  };
};

Then, rewrite plugin/denops-helloworld.vim to automatically call the init API on plugin load via the DenopsPluginPost:{plugin_name} autocmd:

if exists('g:loaded_denops_helloworld')
  finish
endif
let g:loaded_denops_helloworld = 1

augroup denops_helloworld
  autocmd!
  autocmd User DenopsPluginPost:denops-helloworld
      \ call denops#notify('denops-helloworld', 'init', [])
augroup END

Once Vim is restarted, the DenopsHello command will be registered.

Then you can run:

:DenopsHello Your name

If the plugin has been registered successfully, you will see Hello, Your name! as a result.

Next Steps

In the next step, follow the tutorial to learn how to develop a real Denops plugin.

Tutorial (Maze)

Now that you have grasped the basics of developing Denops plugins in the previous chapters, it's time to delve into creating a more functional plugin.

So, out of the blue, have you ever felt the urge to solve mazes while programming? Personally, I haven't, but there might be enthusiasts who enjoy it immensely. In any case, let's embark on developing a Denops plugin that can generate and display a maze in Vim at any time. Well, don't ask me why we would want to do that.

note

The plugin we will create in this chapter can be found at https://github.com/vim-denops/denops-maze.vim

Utilizing Third-Party Library

Certainly, starting with coding a maze generation algorithm would be nice. However, since you're now using Deno, you can conveniently employ a third-party library called maze_generator. Let's define a Maze command similar to DenopsHello; Maze generates a maze and outputs it.

note

The maze_generator library is a third-party library that generates a maze. It is not a part of Deno or Denops. You can use any third-party library that is compatible with Deno in your Denops plugin. Thanks to Deno, developers and users don't need to worry about the installation of third-party libraries. Deno automatically downloads and caches the library when it is imported.

Create the denops-maze plugin and place it under ~/denops-maze. The directory tree will look like this:

~/denops-maze
├── denops
│    └── denops-maze
│           └── main.ts
└── plugin
     └── denops-maze.vim

The content of the denops/denops-maze/main.ts file will be:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    maze() {
      const maze = new Maze({}).generate();
      const content = maze.getString();
      console.log(content);
    },
  };
};

The content of the plugin/denops-maze.vim file will be:

if exists('g:loaded_denops_maze')
  finish
endif
let g:loaded_denops_maze = 1

" Function called once the plugin is loaded
function! s:init() abort
  command! Maze call denops#request('denops-maze', 'maze', [])
endfunction

augroup denops_maze
  autocmd!
  autocmd User DenopsPluginPost:denops-maze call s:init()
augroup END

tip

The Maze command is defined once the plugin is loaded with the above code. If you wish to define the command immediately after Vim startup, you can define the command and use denops#plugin#wait() or denops#plugin#wait_async() in the function to wait for plugin load, like this:

if exists('g:loaded_denops_maze')
  finish
endif
let g:loaded_denops_maze = 1

function! s:maze() abort
  if denops#plugin#wait('denops-maze')
     " Something went wrong
     return
  endif
  call denops#request('denops-maze', 'maze', [])
endfunction

command! Maze call s:maze()

Don't forget to activate the plugin by adding the following line to your vimrc:

set runtimepath+=~/denops-maze

Then, restart Vim and execute :Maze to see the generated maze. Note that it may take a few seconds for the first startup because Deno will download the dependencies, but it happens only once.

Utilizing Third-Party Library

Outputting Content to a Buffer

In the previous section, we echoed the maze to the echo area. However, echoing the maze to the echo area is not very practical. In this section, we will output the maze to a buffer so that users can yank the maze with daily Vim operations!

Let's modify the code to make the generated maze output to a buffer.

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async maze() {
      const maze = new Maze({}).generate();
      const content = maze.getString();
      await denops.cmd("enew");
      await denops.call("setline", 1, content.split(/\r?\n/g));
    },
  };
};

In this code, denops.cmd executes the Vim command enew to open a new buffer in the current window. Then, denops.call calls the Vim function setline() to write the maze to the buffer.

Restart Vim, rerun the Maze command, and then you can see:

Outputting Content to a Buffer

Adjusting Maze Size to Fit the Window

In the previous section, we outputted the maze to a buffer. However, the maze size can sometimes be too large or too small for the window. It would be better to have a maze that fits the current window size.

Let's modify the plugin to ensure the generated maze fits the current window size.

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async maze() {
      await denops.cmd("enew");

      const winWidth = await fn.winwidth(denops, 0);
      const winHeight = await fn.winheight(denops, 0);
      const maze = new Maze({
        xSize: winWidth / 3,
        ySize: winHeight / 3,
      }).generate();
      const content = maze.getString();

      await fn.setline(denops, 1, content.split(/\r?\n/g));
    },
  };
};

In this code, we utilize the function module (aliased to fn) of @denops/std (Denops Standard Library) to call winwidth(), winheight(), and setline() functions. Then, we create a maze that fits the current window size and write it to the buffer.

So why do we use the function module instead of denops.call? With denops.call, developers must know the function name, arguments, return type, and manually cast the return value to the expected type (like as string). However, with the function module, developers can use auto-completion, type checking, etc. It is more convenient and safe to use the function module.

tip

The function module of the @denops/std library provides a set of functions that are available on both Vim and Neovim. If you'd like to use Vim or Neovim only functions, use the vim or nvim module under the function module instead.

See the function module of @denops/std API document for more details.

Restart Vim, rerun the :Maze command, and then you can see:

Properly Create a Virtual Buffer

Now that the maze is displayed in a buffer, but it is not properly configured. For example, if a user executes the :edit command on the buffer, the maze will disappear. This is because Vim does not know how to reload the buffer content, and we must inform Vim about the content of the buffer when it is reloaded.

In this section, we will use the buffer module of @denops/std to create a proper virtual buffer that concretizes the buffer content. Let's modify the main.ts file as follows:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async maze() {
      const { bufnr, winnr } = await buffer.open(denops, "maze://");

      const winWidth = await fn.winwidth(denops, winnr);
      const winHeight = await fn.winheight(denops, winnr);
      const maze = new Maze({
        xSize: winWidth / 3,
        ySize: winHeight / 3,
      }).generate();
      const content = maze.getString();

      await buffer.replace(denops, bufnr, content.split(/\r?\n/g));
      await buffer.concrete(denops, bufnr);
    },
  };
};

In this code, we use buffer.open to open a maze:// buffer and get the buffer number (bufnr) and the window number (winnr). Because Denops works asynchronously, the current buffer or window may be changed from what we expected. That's why developers should use buffer.open to open a buffer and save the buffer number and the window number for further operations.

Then, we call fn.winwidth and fn.winheight with the obtained window number to get the window size. Again, the current window might be changed, so we should use winnr to specify the window.

note

Vim may execute some events between RPC calls, so the current buffer or window really may be changed from what we expected. Denops plugin developers should be careful about this. The best practice for avoiding this problem is to avoid using current and always specify the buffer number or window number.

After that, we use buffer.replace to replace the content of the buffer. Actually, replacing the buffer content is a bit tricky. Developers should care about modifiable options to avoid unmodifiable errors, foldmethod options to keep foldings, and should remove the buffer content that is not replaced by setline or setbufline, etc. The buffer.replace function will care about all of those, so developers should avoid using setline or setbufline directly.

At the end, we call buffer.concrete to concretize the buffer content. This function defines BufReadCmd autocmd to restore the content when the buffer is reloaded. Without this, the buffer content will be discarded when the user executes the :edit command.

Restart Vim, rerun the :Maze command, and then you can see:

Try the :edit command on the buffer, and you can see the maze is still there.

Properly Configure the Buffer

In the previous section, we didn't configure the buffer options, so the buffer remains modifiable and persists after being closed. In this section, we will configure the buffer options to make the buffer non-modifiable and remove the buffer after closure. Open the main.ts file and modify the maze method as follows:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import * as op from "jsr:@denops/std@^7.0.0/option";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async maze() {
      const { bufnr, winnr } = await buffer.open(denops, "maze://");

      const winWidth = await fn.winwidth(denops, winnr);
      const winHeight = await fn.winheight(denops, winnr);
      const maze = new Maze({
        xSize: winWidth / 3,
        ySize: winHeight / 3,
      }).generate();
      const content = maze.getString();

      await buffer.replace(denops, bufnr, content.split(/\r?\n/g));
      await buffer.concrete(denops, bufnr);

      await op.bufhidden.setLocal(denops, "wipe");
      await op.modifiable.setLocal(denops, false);
    },
  };
};

In this code, we use op.bufhidden.setLocal to set the bufhidden option to wipe so that the buffer is wiped out when it is closed. Additionally, we use op.modifiable.setLocal to set the modifiable option to false to make the buffer non-modifiable. Note that since we use buffer.replace to replace the content of the buffer, there is no need to explicitly set the modifiable option to true before replacing the content.

Restart Vim, rerun the :Maze command, and confirm that the buffer is not modifiable.

Reducing the Number of RPC Calls

As Denops employs RPC to interact with Vim, the volume of RPC calls significantly influences the plugin's performance. In this section, we aim to enhance performance by reducing the number of RPC calls using the batch module from @denops/std. Let's revise the main.ts file as follows:

import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { batch, collect } from "jsr:@denops/std@^7.0.0/batch";
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import * as op from "jsr:@denops/std@^7.0.0/option";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";

export const main: Entrypoint = (denops) => {
  denops.dispatcher = {
    async maze() {
      const { bufnr, winnr } = await buffer.open(denops, "maze://");

      const [winWidth, winHeight] = await collect(denops, (denops) => [
        fn.winwidth(denops, winnr),
        fn.winheight(denops, winnr),
      ]);
      const maze = new Maze({
        xSize: winWidth / 3,
        ySize: winHeight / 3,
      }).generate();
      const content = maze.getString();

      await batch(denops, async (denops) => {
        await buffer.replace(denops, bufnr, content.split(/\r?\n/g));
        await buffer.concrete(denops, bufnr);
        await op.bufhidden.setLocal(denops, "wipe");
        await op.modifiable.setLocal(denops, false);
      });
    },
  };
};

In this code, we use the collect function to gather window size values and the batch function to execute multiple commands in a single RPC. This optimization significantly reduces the number of RPC calls, thereby improving the plugin's performance.

The collect function is designed for collecting multiple values in a single RPC, offering the following features:

  • Execution of denops.call or denops.eval within the collect is delayed and executed in a single RPC with the results.
  • The result of denops.call or denops.eval in the collect is always falsy, indicating that branching (if, switch, etc.) is not allowed.
  • Execution of denops.redraw or denops.cmd in the collect is not allowed.
  • Execution of batch or collect in the collect is not allowed, indicating that nesting is not allowed.

In short, only the following operations are allowed in the collect:

  • denops.call or denops.eval that returns a value.
  • Functions in the function module that return a value.
  • Functions in the option module that return a value.
  • Functions in the variable module that return a value.

The batch function is designed for executing multiple commands in a single RPC, offering the following features:

  • Execution of denops.call, denops.cmd, or denops.eval in the batch is delayed and executed in a single RPC without the results.
  • The result of denops.call or denops.eval in the batch is always falsy, indicating that branching (if, switch, etc.) is not allowed.
  • Execution of denops.redraw is accumulated and only executed once at the end of the batch.
  • Execution of batch in the batch is allowed, indicating that nesting is allowed.
  • Execution of collect in the batch is not allowed, indicating that nesting is not allowed.

In short, only the following operations are allowed in the batch:

  • denops.call, denops.cmd, or denops.eval (without the results).
  • Functions in the function module (without the results).
  • Functions in the option module (without the results).
  • Functions in the variable module (without the results).
  • Functions in other modules that do not call collect internally.

In the previous code, the number of RPC calls was more than 7, but after using batch and collect, the number of RPC calls is reduced to 3. Although this is a small plugin, the performance improvement may not be noticeable. However, in a larger plugin, the performance improvement will be significant.

Restart Vim, rerun the :Maze command, and confirm that the plugin works properly with batch and collect.

Next Steps

In the next step, read API references or real-world plugins

API Reference

There is a standard module @denops/std to develop denops plugins. It provides various functions to interact with Vim and Neovim and some shorthands to make it easier to write plugins.

You can find API references about the module by checking the Deno doc page: https://jsr.io/@denops/std.

FAQ

How to Check Denops Startup Time

To check the startup time of Denops or Denops plugins, utilize denops-startup-recorder. This plugin visualizes the timing of events related to Denops and Denops plugin startup.

It shows the result in echo area like:

How to Check Denops Performance

To assess Denops performance, employ denops-benchmark. This plugin measures the number of operations or characters that can be processed in milliseconds.

It shows the result in a buffer like: