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:
- DOOM-NVIM (572K effective code)
- LunarVim (452K effective code)
- AstroNvim (316K effective code)
- NvChad (108K effective code)
- SRC: https://github.com/NvChad/NvChad
- WIKI: https://github.com/NvChad/NvChad/wiki (old)
- HOME: https://nvchad.netlify.app/
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.
/home/<username>/.config/nvim
/home/<username>/.local/share/nvim/site/pack/packer/opt/*
/home/<username>/.local/share/nvim/site/pack/packer/opt/*/after
/home/<username>/.config/nvim/after
/home/<username>/.config/astronvim
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
- Function:
conditional_func
- Function:
user_plugin_opts
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
- If
condition
is not specified, and iffunc
is the function type, returnfunc(...)
. - If
condition
exists and is met, and iffunc
is the function type, returnfunc(...)
.
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.
module="polish"
default=nil
extend=false
prefix=nil
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.
- Function:
load_module_file
- Load module from extended RTP containing
~/.config/astronvim
.
- Load module from extended RTP containing
- Function:
func_or_exten
- Mix system and user configuration
- Function:
user_setting_table
- Load user setting from offset path with additional
/user/
- Load user setting from offset path with additional
- Function:
vim.tbl_deep_extend
- See
:help tbl_deep_extend
- Merges recursively two or more map-like tables.
- See
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.
- The
DEFAULT
contains a table setting the upstream default values. - The
"MODULE"
contains a string specifying user settings by the module or variable name. - If the module named
user.MODULE
exists, then AstroNvim obtains user settings from theuser.MODULE
module.- This is the Splitting Up Configuration case.
- If the
EXTEND
isnil
(missing) ortrue
, settings are extended:- If the
user.MODULE
module returns a table, then AstroNvim assigns the returned table to theMODULE
variable, and generates settings by calling thevim.tbl_deep_extend("force", DEFAULT, MODULE)
function extending theDEFAULT
table theMODULE
table. - If the
user.MODULE
module returns a function, then AstroNvim assigns the returned function to theMODULE
variable, and generates custom extended settings by calling theMODULE
function with theDEFAULT
as its argument.
- If the
- If the
EXTEND
isfalse
, settings are overridden:- If the
user.MODULE
module returns a table, then AstroNvim ignoresDEFAULT
and generates settings from theMODULE
table. - If the
user.MODULE
module returns a function, then AstroNvim ignoresDEFAULT
and generates settings by executing theMODULE
function.
- If the
- If the module named
user.MODULE
doesn’t exist, then AstroNvim obtains user settings from theuser/init.lua
file while looking for theMODULE
variable in it.- This is the single setting file case using the
user/init.lua
file as discussed in Basic Configuration. - If the
EXTEND
isnil
(missing) ortrue
, settings are extended:- If the
MODULE
variable contains a table, then AstroNvim generates settings by calling thevim.tbl_deep_extend("force", DEFAULT, MODULE)
function extending theDEFAULT
table by theMODULE
table. - If the
MODULE
variable contains a function, then AstroNvim generates custom extended settings by calling theMODULE
function with theDEFAULT
as its argument.
- If the
- If the
EXTEND
isfalse
, settings are overridden:- If the
MODULE
variable contains a table, then AstroNvim ignoresDEFAULT
and generates settings from theMODULE
table. - If the
MODULE
variable contains a function, then AstroNvim ignoresDEFAULT
and generates settings by executing theMODULE
function.
- If the
- This is the single setting file case using the
- If neither the module named
user.MODULE
nor the variable namedMODULE
in theuser/init.lua
file exist, then AstroNvim generates settings from the original upstreamDEFAULT
.
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.
- Toggle line number
- Toggle toggle_signcolumn
- …
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:
- split configuration
- indent blankline just with highlight (for better copy-and-paste)
- blinking guicursor for insert
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
Now (2023-11-12), Neovim supports NVIM_APPNAME. Use this to support 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 |