In a previous post I mentoned about bringing the old vim config to neovim by making symlinks between ~/.vim to ~/.config/nvim as well as ~/.vimrc to ~/.config/nvim/init.vim. But as I see neovim getting mature, seems I don’t need to care about vim config any more. While the .vim files are in vim’s own syntax, neovim comes with lua as the alternative configuration language.

Lua the language

Lua is simply an embeddeding langauge. It has simple syntax that fits as a scripting language. And there are lua libraries to help a program written in C to export internal data structure to lua and run a lua script that uses them. Hence it is a way to let user config something without recompiling. It is therefore a perfect way to let a program add plugins.

The best reference to using lua with neovim seems to be the series Learn neovim the practical way. Then the pointers for more advanced use, such as writing a neovim plugin using lua, can be found in https://github.com/nanotee/nvim-lua-guide.

Neovim config and plugins

Neovim has the config located at ~/.config/nvim and the plugins, undo file, and so on located at ~/.local/share/nvim. Indeed those are the paths under the environment variables $XDG_CONFIG_HOME and $XDG_DATA_HOME as specified by FreeDesktop, which neovim honours.

We can make a default neovim config file in lua by naming the file init.lua instead of init.vim in ~/.config/nvim. All other lua scripts that loaded from init.lua should be placed at lua/ subdirectory.

Following the design of the series above, below is my ~/.config/nvim/init.vim, with only one line, and defer everything else to ~/.config/nvim/lua/plugins.lua:

require("plugins").setup()

Now Packer seems to be a better plugin management system for neovim. Below are the content of ~/.config/nvim/lua/plugins.lua, containing my selection of plugins:

local M = {}

function M.setup()
  -- Indicate first time installation
  local packer_bootstrap = false

  -- packer.nvim configuration
  local conf = {
    profile = {
      enable = true,
      threshold = 1, -- the amount in ms that a plugins load time must be over for it to be included in the profile
    },
    display = {
      open_fn = function()
        return require("packer.util").float { border = "rounded" }
      end,
    },
  }

  -- Check if packer.nvim is installed
  -- Run PackerCompile if there are changes in this file
  local function packer_init()
    local fn = vim.fn
    local install_path = fn.stdpath "data" .. "/site/pack/packer/start/packer.nvim"
    if fn.empty(fn.glob(install_path)) > 0 then
      packer_bootstrap = fn.system {
        "git",
        "clone",
        "--depth",
        "1",
        "https://github.com/wbthomason/packer.nvim",
        install_path,
      }
      vim.cmd [[packadd packer.nvim]]
    end
    vim.cmd "autocmd BufWritePost plugins.lua source <afile> | PackerCompile"
  end

  -- Plugins
  local function plugins(use)
    use { "wbthomason/packer.nvim" }

    -- load plugins only when needed
    use { "nvim-lua/plenary.nvim", module = "plenary" }

    -- precompile plugins to bytecode to speed up neovim start
    use { "lewis6991/impatient.nvim" }

    -- Better Comment
    use {
      "numToStr/Comment.nvim",
      opt = true,
      keys = { "gc", "gcc", "gbc" },
      config = function()
        require("Comment").setup {}
      end,
    }

    -- Markdown
    use {
      "iamcco/markdown-preview.nvim",
      run = function()
        vim.fn["mkdp#util#install"]()
      end,
      ft = "markdown",
      cmd = { "MarkdownPreview" },
    }

    -- Git
    use {
      "TimUntersberger/neogit",
      requires = "nvim-lua/plenary.nvim",
      config = function()
        require("config.neogit").setup()
      end,
    }

    -- Better icons
    use {
      "kyazdani42/nvim-web-devicons",
      module = "nvim-web-devicons",
      config = function()
        require("nvim-web-devicons").setup { default = true }
      end,
    }

    -- Status line
    use {
      "nvim-lualine/lualine.nvim",
      event = "VimEnter",
      config = function()
        require("config.lualine").setup()
      end,
      requires = { "nvim-web-devicons" },
    }

    -- Treesitter
    use {
      "nvim-treesitter/nvim-treesitter",
      run = ":TSUpdate",
      config = function()
        require("config.treesitter").setup()
      end,
    }
    use {
      "SmiteshP/nvim-gps",
      requires = "nvim-treesitter/nvim-treesitter",
      module = "nvim-gps",
      config = function()
        require("nvim-gps").setup()
      end,
    }

    -- neorg for org-mode
    use {
      "nvim-neorg/neorg",
      -- tag = "latest",
      ft = "norg",
      after = "nvim-treesitter", -- You may want to specify Telescope here as well
      config = function()
        require('neorg').setup {
          -- Tell Neorg what modules to load
          load = {
            ["core.defaults"] = {}, -- Load all the default modules
            ["core.norg.concealer"] = {}, -- Allows for use of icons
            ["core.norg.dirman"] = { -- Manage your directories with Neorg
              config = {
                workspaces = {
                  work = "~/_neorg"
                }
              }
            }
          },
        }
      end
    }

    -- Terminal
    use {
      "akinsho/toggleterm.nvim",
      keys = { [[<C-\>]] },
      cmd = { "ToggleTerm", "TermExec" },
      config = function()
        require("config.toggleterm").setup()
      end,
    }

    -- AI completion
    use { "github/copilot.vim", event = "InsertEnter" }

    -- Bootstrap Neovim
    if packer_bootstrap then
      print "Restart Neovim required after installation!"
      require("packer").sync()
    end
  end

  packer_init()

  local packer = require "packer"
  packer.init(conf)
  packer.startup(plugins)
end

return M

The Packer system allows you to run :PackerCompile, :PackerSync, and :PackerClean for maintenance, or :PackerInstall to install new plugins added to, or uninstall old plugins that removed from this config. So after this, we simply load up neovim, do :PackerInstall and then :PackerCompile to finish the set up.

The other part that is important for neovim is the subdirectory, ~/.config/nvim/after/plugins/, which all lua scripts there will be run after the init.lua is executed. My old vim config is like the following:

set mouse=a                 " mouse in normal, visual, and insert mode
set expandtab               " converts tabs to white space
set nocompatible            " disable compatibility to old-time vi
set showmatch               " show matching brackets.
set ignorecase              " case insensitive matching
set hlsearch                " highlight search results
set tabstop=4               " number of columns occupied by a tab character
set softtabstop=4           " see multiple spaces as tabstops so <BS> does the right thing
set shiftwidth=4            " width for autoindents
set autoindent              " indent a new line the same amount as the line just typed
set number                  " add line numbers
set cursorline
set spelllang=en_us
syntax on                   " syntax highlighting

" mouse release send selection to clipboard
vmap <LeftRelease> "*ygv

and therefore I make the following into ~/.config/nvim/after/plugin/defaults.lua:

local api = vim.api
local g = vim.g
local opt = vim.opt
local cmd = vim.cmd

-- mouse release = send selection to clipboard
-- nvim_set_keymap() help: see https://github.com/nanotee/nvim-lua-guide
local default_opts = { noremap = true, silent = true }
api.nvim_set_keymap("v", "<LeftRelease>", '"*ygv', default_opts)

opt.termguicolors = true   -- Enable colors in terminal
opt.hlsearch = true        -- Set highlight on search
opt.number = true          -- Make line numbers default
opt.relativenumber = true  -- Make relative number default
opt.mouse = "a"            -- Enable mouse mode
opt.breakindent = true     -- Enable break indent
opt.undofile = true        -- Save undo history
opt.ignorecase = true      -- Case insensitive searching unless /C or capital in search
opt.smartcase = true       -- Smart case
opt.updatetime = 250       -- Decrease update time
opt.signcolumn = "yes"     -- Always show sign column
-- opt.clipboard = "unnamedplus" -- Access system clipboard
opt.expandtab = true       -- use space instead of tab by default
opt.showmode = false       -- Do not need to show the mode. We use the statusline instead.
opt.showmatch = true       -- highlight matching brackets
opt.cursorline = true      -- show cursor line
opt.cursorcolumn = true    -- show cursor column
opt.joinspaces = false     -- No double spaces with join after a dot
opt.list = true            -- show space and tabs chars
opt.listchars = "eol:⏎,tab:▸·,trail:×,nbsp:⎵"  -- make tab, etc visible
opt.spelllang = "en_us"
opt.sessionoptions = "buffers,curdir,folds,help,tabpages,winsize,winpos,terminal"

-- prefer `industry` color scheme
cmd [[
  colorscheme industry
]]

which in lua, the :set command becomes vim.opt and if we need to type some : commands, we wrap it inside vim.cmd. There are some more goodies from the Medium series following the link above. One useful item is the enhanced status line, which depends on the plugin lualine. The following is the config ~/.config/nvim/lua/config/lualine.lua:

local M = {}

function M.setup()
  local gps = require "nvim-gps"

  require("lualine").setup {
    options = {
      icons_enabled = true,
      theme = "auto",
      component_separators = { left = "", right = "" },
      section_separators = { left = "", right = "" },
      disabled_filetypes = {},
      always_divide_middle = true,
      globalstatus = false, -- global line = shared across all window
    },
    sections = {
      lualine_a = { "mode" },
      lualine_b = { "branch", "diff", "diagnostics" },
      lualine_c = {
        { "filename" },
        {
          gps.get_location,
          cond = gps.is_available,
          color = { fg = "#f3ca28" },
        },
      },
      lualine_x = { "encoding", "fileformat", "filetype" },
      lualine_y = { "progress" },
      lualine_z = { "location" },
    },
    inactive_sections = {
      lualine_a = {},
      lualine_b = {},
      lualine_c = { "filename" },
      lualine_x = { "location" },
      lualine_y = {},
      lualine_z = {},
    },
    tabline = {},
    extensions = {},
  }
end

return M

Since it is to make a fancy status line, some unicode characters are used here. Part of it is from the Powerline for vim, which are used in the separators specified above. The other are the web devicons, that are less commonly used. I am using iTerm2, which can emulate Powerline fonts by checking “Use built-in Powerline glyphs” in Preferences→Profiles→Text. But the devicons really need a font to support. There are some nerdfonts that we can download from https://www.nerdfonts.com/font-downloads but not all have the full set of icons. The one I tried that works is DejaVuSansMono.

Using OpenSSL automatically

This is the interesting part. Copied from https://vim.fandom.com/wiki/Encryption, in the VIM script we can do the following to automatically run a command upon open and close of a buffer to edit a password-encrypted file:

augroup CPT
  au!
  au BufReadPre  *.cpt setl bin viminfo= noswapfile
  au BufReadPost *.cpt let $CPT_PASS = inputsecret("Password: ")
  au BufReadPost *.cpt silent 1,$!ccrypt -cb -E CPT_PASS
  au BufReadPost *.cpt set nobin
  au BufWritePre *.cpt set bin
  au BufWritePre *.cpt silent! 1,$!ccrypt -e -E CPT_PASS
  au BufWritePost *.cpt silent! u
  au BufWritePost *.cpt set nobin
augroup END

the following is my version in Lua script for neovim, which uses OpenSSL for encryption:

local api = vim.api

-- encryption file
--    dec command: openssl enc -in "$1" -out $TXTFILE -k "$PW" -d -aes-256-cbc
--    enc command: openssl enc -in $TXTFILE -out "$1" -k $PW -e -aes-256-cbc
local cryptGrp = api.nvim_create_augroup("EncryptFile", { clear = true })
api.nvim_create_autocmd("BufReadPre", {
  pattern = '*.enc',
  command = [[setl bin viminfo= noswapfile]],
  group = cryptGrp,
})
api.nvim_create_autocmd("BufReadPost", {
  pattern = '*.enc',
  command = [[
    let $ENC_PASS = inputsecret("Password: ")
    silent 1,$!openssl enc -k $ENC_PASS -d -aes-256-cbc
    if v:shell_error > 0
      bdelete!
      throw "Bad decryption"
    endif
    set nobin
  ]],
  group = cryptGrp,
})
api.nvim_create_autocmd("BufWritePre", {
  pattern = '*.enc',
  command = [[
    set bin
    silent 1,$!openssl enc -k $ENC_PASS -e -aes-256-cbc
  ]],
  group = cryptGrp,
})
api.nvim_create_autocmd("BufWritePost", {
  pattern = '*.enc',
  command = [[
    silent 1,u
    set nobin
  ]],
  group = cryptGrp,
})

This lua script is saved in ~/.config/nvim/after/plugin/. Those are hooked to files with the extension enc. We used the vi function inputsecret() to get a password and assign to a variable (persistent until end of session). Then invoking openssl command to transcode the current buffer from ciphertext to plaintext. If the command failed, which usually is caused by bad password, we close the buffer automatically to prevent accidental damage to the original file. Similarly before we write, we use the command to transcode plaintext into ciphertext, reusing the password we opened the file. Then after we write, we undo the transcode (u). This should show enough for how the old vim script correspond to the lua script in neovim.

If you use this, be careful the password is saved in $ENC_PASS environment variable. This will be a security risk as well as it can be overwritten when you opened another encrypted buffer (i.e., you’re going to mess the files if you want to edit multiple encrypted files with different password)

An improvement would be to make some edit:

api.nvim_create_autocmd("BufReadPost", {
  pattern = '*.enc',
  command = [[
    let b:enc_pass = inputsecret("Password: ")
    let $ENC_PASS = b:enc_pass
    silent 1,$!openssl enc -k $ENC_PASS -d -aes-256-cbc
    let $ENC_PASS = ""
    if v:shell_error > 0
      bdelete!
      throw "Bad decryption"
    endif
    set nobin
  ]],
  group = cryptGrp,
})
api.nvim_create_autocmd("BufWritePre", {
  pattern = '*.enc',
  command = [[
    set bin
    let $ENC_PASS = b:enc_pass
    silent 1,$!openssl enc -k $ENC_PASS -e -aes-256-cbc
    let $ENC_PASS = ""
  ]],
  group = cryptGrp,
})

This way, we create a neovim internal variable in each buffer, b:enc_pass, to host the encryption password and set it to an environment variable only when we invoke OpenSSL command. It is not perfect, but at least allows different password for different buffer. We may avoid the environment variable by using execute command in vim but we may nedd to escape the passsword to make a valid command string.