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.