Re-learning Vim (4)

Date: 2022/07/15 (initial publish), 2022/12/23 (last update)

Previous Post Top Next Post

TOC

NOTE: As of 2022-12-23, I use NeoVim (v0.8.1 upstream deb) with AstroNvim (v2.10.1). Upstream of AstroNvim has included many things written below as a part of its official documentation and adopted new features proposed below.

Neovim 0.7 migration

After short trial of Neovim (nvim) 0.5 described in Re-learning Vim (3), I went back to the good old Vim with ALE.

As I find out Neovim (nvim) 0.7 now has native LSP support and tools around it seems to be getting mature, I decided to check nvim with lua again. I also found a nice recent review:

Since AstroNvim seems to be interesting for its compactness and pre-configured package settings, I tried it as the main nvim configuration.

Then I checked code size etc on IDE system:

All these already use delayed loading etc., to save their start up time. That is not what I am focused any more. My focus is now the out-of-box usability and the ease of configuration.

Syntax checker

If I install shellcheck,AstroNvim displays error messages. Nice out of box configuration.

Internals of AstroNvim

Let me record how I looked into the internals of AstroNvim for its user configuration.

Prerequisite

See Getting started using Lua in Neovim and its linked documents first.

Lua basics

Please note . in the lua module path corresponds to / in the directory path. Also the foo.bar lua module may be at $runtimepath/foo/bar.lua or $runtimepath/foo/bar/init.lua.

For the definition of vim.tbl_deep_extend, see :help tbl_deep_extend.

For the actual definition of user_plugin_opts and related functions, see their definition in lua/core/utils/init.lua.

Value of runtimepath option

The AstroNvim start-up code extends the value of runtimepath and it contains not only $XDG_CONFIG_HOME/nvim but also $XDG_CONFIG_HOME/astronvim. Here, $XDG_CONFIG_HOME is normally set to ~/.config.

This allows us to place user settings under the normal path of ~/.config/nvim/lua/user and the offset path of ~/.config/astronvim/lua/user.

Its value can be verified by:

:lua print(vim.inspect(vim.opt.runtimepath))

It is impossible to read so much output and make any sense. Here, :redir can be used to ease reading of these by capturing output in a file. (The use of :silent kills pager prompt.)

:redir > rtp.txt
:silent lua print(vim.inspect(vim.opt.runtimepath))
:redir END

Now we know scope = "global", shortname = "rtp", etc. We can always use short name, too.

If we need its current value only, it can be inspected and presented in lua table by:

:lua print(vim.inspect(vim.opt.rtp:get()))

Alternatively since we now know it is a global option, it can be accessed and presented in similar way as vim classic :set rtp? style by:

:lua print(vim.go.rtp)

From this output, we can confirm rtp to be scanned roughly as follows.

The notable point is the last one specially inserted by AstroNvim.

This allows us to move lua/user/ directory out of ~/config/nvim to ~/config/astronvim.

Tracing source code of AstroNvim with single ~/.config/astronvim/user/init.lua

Let’s try to trace source code of AstroNvim to understand how to customize it.

Let me first assume there is a user configuration file at ~/.config/astronvim/user/init.lua for simplicity.

Entry point: init.lua

The entry point of nvim is /home/<username>/.config/nvim/init.lua. It is simply as:

local impatient_ok, impatient = pcall(require, "impatient")
if impatient_ok then impatient.enable_profile() end

for _, source in ipairs {
  "core.utils",
  "core.options",
  "core.bootstrap",
  "core.plugins",
  "core.autocmds",
  "core.mappings",
  "core.ui",
  "configs.which-key-register",
} do
  local status_ok, fault = pcall(require, source)
  if not status_ok then vim.api.nvim_err_writeln("Failed to load " .. source .. "\n\n" .. fault) end
end

astronvim.conditional_func(astronvim.user_plugin_opts("polish", nil, false))

The first few lines are for impatient.nvim package. Its one of the package installed in /home/<username>/.local/share/nvim/site/pack/packer/start/ together with packer.nvim and popup.nvim. These are loaded automatically upon starting nvim.

impatient.nvim speeds up loading Lua modules in Neovim to improve startup time.

Then loop over source to set up AstroNvim’s upstream configured system. We will get back to these.

The last line is entry point for the user configuration. So we need to find out followings:

Variable astronvim

The astronvim is the globally accessible variable within AstroNvim defined originally in utils/init.lua as:

_G.astronvim = {}

We can inspect its current value as:

:lua print(vim.inspect(astronvim))

(It is too big to paste here but it is full of important informaion)

For checking user setting, use the following:

:lua print (vim.inspect(astronvim.user_settings))

For checking “mappings” only, use the following:

:lua print (vim.inspect(astronvim.user_settings.mappings))

For checking “which-key” only, use the following:

:lua print (vim.inspect(astronvim.user_settings["which-key"]))

Function: conditional_func

This is a simple function defined in lua/core/utils/init.lua

function astronvim.conditional_func(func, condition, ...)
  if (condition == nil and true or condition) and type(func) == "function" then return func(...) end
end

The above case is, condition=nil and ... is nil. So this simply calls astrovim.user_plugin_opts("polish", nil, false) at the end of the nvim start up.

Function: user_plugin_opts in ~/.config/nvim/init.lua

This is a function defined in lua/core/utils/init.lua

function astronvim.user_plugin_opts(module, default, extend, prefix)
  if extend == nil then extend = true end
  default = default or {}
  local user_settings = load_module_file((prefix or "user") .. "." .. module)
  if user_settings == nil and prefix == nil then user_settings = user_setting_table(module) end
  if user_settings ~= nil then default = func_or_extend(user_settings, default, extend) end
  return default
end

Since in lua/core/utils/init.lua, following values are used.

Thus, this function processes essentially as:

  module="polish"
  default = {}
  extend=false
  prefix=nil
  local user_settings = load_module_file("user.polish")
    -- nil with ~/.config/astrovim/lua/usr/init.lua only configuration
  if user_settings == nil then user_settings = user_setting_table("polish") end
    -- load ~/.config/astrovim/lua/usr/init.lua and return "polish" as
    -- user_settings with some tricks
  if user_settings ~= nil then default = func_or_extend(user_settings, {} , false) end
    -- update default by user_settings in polish 
  return default

So essentially, if lua/core/utils/init.lua is given, its polish function seems to be executed. with some tricks. (Of courese, 3 local functions used by user_plugin_opts needs to be examined carefully as follows to come to this conclusion.)

3 local functions used by user_plugin_opts

Let’s understand 3 local functions and 1 vim’s lua binding function used by user_plugin_opts in lua/core/utils/init.lua first.

Key part of codes are commented as follows:

astronvim.install = { home = stdpath "config" } -- ~/.config/nvim
astronvim.install.config = stdpath("config"):gsub("nvim$", "astronvim") -- ~/.config/`astronvim
vim.opt.rtp:append(astronvim.install.config) -- extend rtp to include ~/.config/astronvim
local supported_configs = { astronvim.install.home, astronvim.install.config }
 -- support both ~/.config/nvim and ~/.config/`astronvim

local function load_module_file(module)
  local found_module = nil
  for _, config_path in ipairs(supported_configs) do -- loop over 2 paths
    local module_path = config_path .. "/lua/" .. module:gsub("%.", "/") .. ".lua"
      -- module name with "." -> directory path with "/".
    if vim.fn.filereadable(module_path) == 1 then found_module = module_path end
  end
  if found_module then -- if module file exists, load it safely with pcall.
    local status_ok, loaded_module = pcall(require, module)
    if status_ok then
      found_module = loaded_module
    else
      astronvim.notify("Error loading " .. found_module, "error")
    end
  end
  return found_module
end

This extends load module capability not only from /.config/nvim but also ~/.config/astronvim.

astronvim.user_settings = load_module_file "user.init"
  -- load from "user/init.lua" in `/.config/nvim` or `~/.config/`astronvim`.
local function func_or_extend(overrides, default, extend)
  if extend then -- false for this part of code run
    if type(overrides) == "table" then
      default = vim.tbl_deep_extend("force", default, overrides)
    elseif type(overrides) == "function" then
      default = overrides(default)
    end
  elseif overrides ~= nil then
    default = overrides
  end
  return default
end

This seems to be the most ingeneous part of AstroNvim for user override mechanism. For override, if corresponding user module name is given and such module exists, that result of user module is returned. Otherwise, default in the argument is returned.

This allows upstream code to set default values while a separate user module file ~/.config/astronvim/lua/user/init.lua can override it. No variable abstruction etc. involved. Very nice.

local function user_setting_table(module)
  local settings = astronvim.user_settings or {}
  for tbl in string.gmatch(module, "([^%.]+)") do
    settings = settings[tbl]
    if settings == nil then break end
  end
  return settings
end

This seems to look into inside of user module for particular module name polish for the case under consideration. It seems quite tricky.

See discussion at reddit on astronvim v140. In this, there are some user configuration tips with its mechanism discussion and benchmarks against popular configurations.

other user_plugin_opts

There are many other use of this hook function to enable user configuration.

$ grep -R  -v "^ *local" . 2>/dev/null |grep user_plugin_opts|wc -l
44

For those other user_plugin_opts in places other than ~/.config/init.lua, they don’t use polish but has actual module name found under ~/.config/astronvim/lua/user/ such as plugins.packaer for ~/.config/astrovim/lua/user/plugins/packer.

function astronvim.user_plugin_opts(module, default, extend, prefix)
  module="plugins.packaer"
  default = {some default values given ...}
  extend=nil
  prefix=nil
  local user_settings = load_module_file("plugins.packer")
    -- user_settings = ~/.config/astrovim/lua/usr/plugins/packer.lua user configuration
  if user_settings == nil then user_settings = user_setting_table("polish") end
    -- skip this
  if user_settings ~= nil then default = func_or_extend(user_settings, { ...}, nil) end
    -- replace default by user_settings in  ~/.config/astrovim/lua/usr/plugins/packer.lua 
  return default

In some sense, this is simple replacement of the upstream setting by user module. Now I see where all maulti-file user configuration is coming from.

I think I got some idea now. Let me recap as below.

Shim function and its hook points

Configuration mechanism of AstroNvim uses the shim function astronvim.user_plugin_opts (usually aliased to local variable user_plugin_opts) when setting default values in the upstream source. The resulting default values returned by this shim function are the user requested combination of corresponding upstream and user settings.

Running rg user_plugin_opts at the root of the source tree should reveal many hook points of user_plugin_opts calls in the source where the upstream sets default values.

How AstroNvim works for user settings

Let us describe tersely in plain words how this shim function astronvim.user_plugin_opts works in AstroNvim when it is called as user_plugin_opts("MODULE", DEFAULT, EXTEND) with a twist of oversimplification. This should provide some perspective for how AstroNvim works for user settings.

Customization of AstroNvim

I started to add some features quickly without learning internals of AstroNvim. Features are the ones I created originally for DOOM-NVIM. So I can control nvim behavior with simple key strokes.

My latest updated PR is https://github.com/AstroNvim/AstroNvim/pull/769

In my user configuration ~/.config/astronvim/lua/user/init.lua, my options modification started simply as:

  -- set vim options here (vim.<first_key>.<second_key> =  value)
  options = {
    opt = {
      relativenumber = false, -- unsets vim.opt.relativenumber
      number = false,         -- unsets vim.opt.number
      spell = true,           -- sets   vim.opt.spell
      signcolumn = "no",      -- unsets vim.opt.signcolumn
    },
    g = {
      mapleader = " ", -- sets vim.g.mapleader
    },
  },

For more customization, see my latest configuration at https://github.com/osamuaoki/astronvim-osamu

Notable changes are:

Lazygit

Lazygit is not yet packaged for Debian. I installed it as:

$ sudo aptitude install golang
$ go install github.com/jesseduffield/lazygit@latest
$ cd ~/bin
$ ln -sf ~/go/bin/lazygit lazygit

Multiple Nvim configuration setups

I also installed LunarVim and NvChad.

LunarNvim creates the lvim command which doesn’t interfere with the normal nvim execution. LunarNvim can co-exist with the normal Nvim setup without extra work.

Since Nvim supports XDG Base Directory Specification, I decided to use it to create alternative nvim commands with multiple configuration settings using xdg-install script. I installed multiple configuration setups for AstroNvim and NvChad.

Modifier keys

Neovim 0.7 now correctly distinguishes modifier key combos in its own input processing, so users can now map e.g. <Tab> and <C-I> separately.

Paste mode

By obsoleting needs for paste mode, Neovim deprecated it. Neovim can tell difference between actual key input from paste input.

Previous Post Top Next Post