Writing Neovim Plugins — A Beginner Guide (Part 1)

A beginner guide on writing Neovim plugins in Lua.

Writing Neovim Plugins


There are many great Neovim plugins but not many guides on writing Neovim plugins in Lua. In this article, let’s start to explore developing Neovim plugins in Lua.

This is a beginner guide and we are going to learn by developing a simple plugin, and continue to explore more advanced topics in future articles.

  • Part 1: This article.
  • Part 2: Available here.

Table of Contents

Startup Options

Let’s first explore different methods to start Neovim without loading vimrc files or plugins.

There are different ways that you can start Neovim without initialization.

  • nvim -u NONE: Start Neovim without loading vimrc files and plugins
  • nvim -u NORC: Start Neovim without loading vimrc files, but still load the plugins.
  • nvim --noplugin: Start Neovim, load vimrc files but not the plugins.
  • nvim --clean: Equivalent to “-u NONE -i NONE”. Skips initializations from files and environment variables. No ‘shada’ file is read or written. Excludes user directories from ‘runtimepath’.

To show you the default runtimepath, I am going to use the--clean option to start Neovim with a clean environment. Type ":h --clean” to check out the documentation.

$ nvim --clean


Type :h rtp to check out the documentation for runtimepath.

runtimepath is a list of directories to be searched for these runtime files. You may already be familiar with some of these directories.

  • autoload: automatically load scripts (:h autoload-functions)
  • colors: color scheme files (h :colorscheme)
  • compiler: compiler files (:h compiler)
  • doc: documentation (:h write-local-help)
  • ftplugin: filetype plugins (:h write-filetype-plugin)
  • pack: packages (:h packadd)
  • plugin: plugin scripts (:h write-plugin)
  • syntax: syntax files (:h mysyntaxfile)
  • lua: Lua files. Only applicable for Neovim (:h lua)

If you install any plugins, you can see that “runtimepath” contains folders of the plugins.

Default runtimepath

To view the runtimepath, you can either

  • type :set rtp?
  • use the expression register. In Insert mode, type Control-R =, and type &rtp, and runtimepath will show in the current buffer.

Setting Runtimepath

To develop plugins, we need to configure runtimepath so that it has the current development directory. There are few options you can set this up.


During startup, Neovim allows you to pass in the command.

$ nvim --cmd "set rtp+=$(pwd)"

Now check again runtimepath and you should be able to see the current folder.

Using Vimscript

Alternatively, after you start up Neovim, you can use the following command to set the runtimepath to the current folder.

:let &runtimepath.=','.escape(expand('%:p:h'), '\,')

Plugin Manager

In most cases, you are likely to use a plugin manager, e.g. vim-plug, or packer.nvim.

Most plugin managers should support local plugins and set the runtimepath accordingly. E.g.

for vim-plug (code snippet available here),

" Local plugins
Plug '~/workspace/development/alpha2phi/alpha.nvim'
runtimepath by vim-plug

for packer.nvim,

-- Local plugins can be included
use '~/projects/personal/hover.nvim'

Use Custom Configuration File

And to use a particular configuration file, you can either

  • Use the $XDG_CONFIG_HOME option during startup.


$ XDG_CONFIG_HOME=$HOME/.xdg_home /usr/local/bin/nvim

The “base” (root) directories conform to the XDG Base Directory Specification. In the above case, Neovim looks for the configuration file under $HOME/.xdg_home/nvim folder.

Type :h $XDG_CONFIG_HOME to read the documentation.

  • Use the -u start-up option to point to the .vim or .lua configuration file.
$ nvim -u /path/to/myconfig.vim
$ nvim -u /path/to/myconfig.lua

Type :h startup-options to read the documentation.

Setting up LSP (Optional)

Since we are developing plugins, it makes sense to set up LSP for Lua and Vimscript.


Refer to this article to set up LSP for Lua.


For Vimscript, we can use the Vim language server. The instructions to set up should be similar to other languages as described in this article.

Plugin Directories Structure

With the runtimepath configured to point to our project directory, let’s create these 3 folders.

  • plugin
  • lua
  • doc

Files inside plugin will each be run once every time Neovim starts. These files are meant to contain code that you always want to be loaded whenever you start NeoVim. Files inside lua contain Lua scripts, and doc folder contains your plugin documentation.

For all the possible directories, type :h rtp.

Now create the following directories and files.

  • plugin/dev.vim
  • plugin/alpha.vim
  • lua/hello/init.lua
  • lua/hello/helloworld.lua
  • doc/alpha.txt

Hello World Plugin

Hello World Module

Add the below lines into lua/hello/helloworld.lua.

local M = {}function M.sayHelloWorld() print('Hello world!!') endreturn M

This is a very simple Lua module. Refer here if you are not familiar with the Lua module.

Add the below lines into lua/hello/init.lua

local hello = require('hello.helloworld')return hello

Type :h lua-require to understand more on Lua require.


Now run :lua require(“hello”).sayHelloWorld() and you should see “Hello world!!” printed.

Hello World Plugin

Module Reloading

Now change lua/hello/helloworld.lua. Instead of “Hello world!!”, change it to “Hello world again!!"

Run :lua require(“hello”).sayHelloWorld() and you will still see “Hello world!!”.

This is because the “hello” module has been loaded by Neovim and does not get refreshed after we make the change.

Now add these lines into plugin/dev.vim

function! ReloadAlpha()
lua << EOF
for k in pairs(package.loaded) do
if k:match("^hello") then
package.loaded[k] = nil
" Reload the plugin
nnoremap <Leader>pra :call ReloadAlpha()<CR>
" Test the plugin
nnoremap <Leader>ptt :lua require("hello").sayHelloWorld()<CR>
  • ReloadAlpha basically unloads the hello module.
  • I also create the key bindings to reload the module and test the plugin.

Now either restart Neovim or source dev.vim by running :so %.

Press <Leader>pra and then <Leader>ptt and you should see the latest change.

Module Reloading

Define Plugin Command

Add these lines into plugin/alpha.vim.

if exists('g:loaded_alpha') | finish | endif " prevent loading file twicelet s:save_cpo = &cpo " save user coptions
set cpo&vim " reset them to defaults
" command to run our plugin
command! AlphaHelloWorld lua require("hello").sayHelloWorld()
let &cpo = s:save_cpo " and restore after
unlet s:save_cpo
let g:loaded_alpha = 1
  • I define the AlphaHelloWorld command to call our hello module
  • Type :h cpo to read the documentation on “compatible options


Add these to doc/alpha.txt.


Publishing Plugin

Publishing the plugin is as simple as committing the project to Github.

plugin/dev.vim is only for development purposes. You may want to exclude this file from commit by including it in .gitignore.

Installation and Testing

Now use your favorite plugin manager to install the plugin.

E.g., using vim-plug,

Plug 'alpha2phi/alpha.nvim'
  • Run “:PlugInstall” to install the plugin.
  • Type “:AlphaHelloWorld” and the “Hello world” message gets printed.
  • Type “:h alpha.nvim” or “:h AlphaHelloWorld” and you should see the help documentation.
Help Documentation

Further Readings


Now we have the “hello world” version of our plugin. In the next article let’s continue to enhance the plugin.

The plugin source code can be found in this repository.


Programmer and occasional blogger.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store