Monday, March 6, 2023

Converting my init.vim to Lua

I've been using Vim and Neovim as my main editor for a bit (i.e., since the late 1990's) and have a small but useful-to-me configuration file. The Vim script configuration has served me well, but some newer Neovim functionality (e.g., the Language Server Protocol) uses Lua. Vim script allows this, but it requires using a here-document:


"""""""""""""""""""""""""""""""""""""""""""""""""
" Search recursively for files from "here"
set path +=**

"""""""""""""""""""""""""""""""""""""""""""""""""
" Set up Language Sever Protocol plugin

lua << EOF
-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
  -- Enable completion triggered by 
  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

While this isn't terrible, it is a bit ugly and evidently, enough so to motivate converting my configuration to Lua.

Plugin Manager

My plugin needs have been modest (cscope bindings and mark) and haven't required a manager. But at some point, mark added a dependency on another package, making having a package manager more useful. vim-plug was a good match and has served me well, and while I'm changing over the init script to Lua, it made sense to use the packer.nvim package manager. Additionally, I moved all plugin definitions to nvim/lua/plugins.lua. This file also includes code to bootstrap packer if needed. After copying the files to a new machine, I use the command

$ nvim --headless -c 'autocmd User PackerComplete quitall' -c 'PackerSync'

to run the bootstrap and install the plugins. The plugins.lua file looks like:

local ensure_packer = function()
  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
    fn.system({'git', 'clone', '--depth', '1', 'https://github.com/wbthomason/packer.nvim', install_path})
    vim.cmd [[packadd packer.nvim]]
    return true
  end
  return false
end

local packer_bootstrap = ensure_packer()

return require('packer').startup(function(use)
    -- Packer can manage itself
    use 'wbthomason/packer.nvim'
    use {
        'inkarkat/vim-mark',
        requires = {'inkarkat/vim-ingo-library'}
    }

    -- Automatically set up your configuration after cloning packer.nvim
    -- Put this at the end after all plugins
    if packer_bootstrap then
        require('packer').sync()
    end
end)

Rosetta Stone

The Set List

The majority of the init.vim consists of set foo lines. This translates in a (mostly) straightforward way, except settings prefixed with "no", drop the "no" and use a value of "false". For example,
set nobackup		" do not keep a backup file, use versions instead
becomes
vim.opt.backup = false	-- do not keep a backup file, use versions instead
and
set title 		" change the window title to be the file name
becomes
vim.opt.title = true	-- change the window title to be the file name

Map to the stars

The configuration has a small number of key mappings. These also translate easily once you understand the syntactic sugar of Vim script. For example,
imap <S-Tab> <Esc> < i
is a mapping while in insert mode. Or
vnoremap  "*ygv
is a non-recursive mapping in visual mode. Armed with this knowledge, filling out the fields in nvim_set_keymap() is straightforward. Our cases above become:
vim.api.nvim_set_keymap("i", " ", "< i", {})
vim.api.nvim_set_keymap("v", "", '"*ygv', { noremap = true }) 

Some examples I found use keymap.set instead of nvim_set_keymap. The later appears to be an older version of the API, but I have chosen to use it as some Linux distributions ship older versions of Neovim which don't support the new syntax.

Bits-n-bobs

There were two other settings that don't fall into the "set" or "map" categories. The first allows recursively searching the current file path. Vim script uses += while Lua uses :append:

"""""""""""""""""""""""""""""""""""""""""""""""""
" Search recursively for files from "here"
set path +=**
" and search the Mercurial directory above if it exists
set path +=../.hg**
becomes
--------------------------------------------------
-- Search recursively for files from "here"
vim.opt.path:append('**')
-- and search the Mercurial directory above if it exists
vim.opt.path:append('../.hg**')

The other restores the cursor to its previous position in the file (see "BufWinEnter" below). The function mapping is as one would expect. The careful observer will notice a few settings disappeared. They are ones that either a) didn't get used or b) are holdovers from Vim.

Overall, this exercise was not too bad. See below for the before and after.

init.vim

" neovim settings
" relies on neovim defaults which are different than vim

set nobackup		" do not keep a backup file, use versions instead
set nowritebackup	" dont want a backup file while editing
set ruler		" show the cursor position all the time
set showcmd		" display incomplete commands
set title 		" change the window title to be the file name
set modeline
set cscopetagorder=0	" search cscope database before tags
set cscopetag		" search cscope database for tags
set nocscopeverbose
set termguicolors
set guicursor=
set background=light
set mouse=a

"""""""""""""""""""""""""""""""""""""""""""""""""
" Plug
call plug#begin()

" Dependency of vim-mark
Plug 'inkarkat/vim-ingo-library'
Plug 'inkarkat/vim-mark'

call plug#end()

" use ESC to close a terminal window
tnoremap <ESC> <C-\><C-n><C-w><C-p>

set clipboard+=unnamedplus
" From the neovim wiki under "clipboard=autoselect is not implemented yet"
" oh, and xsel doesn't seem to work but xclip does
vnoremap <LeftRelease> "*ygv

"""""""""""""""""""""""""""""""""""""""""""""""""
" Use all available colors for highlight (mark.vim)
let g:mwDefaultHighlightingPalette = 'maximum'

"""""""""""""""""""""""""""""""""""""""""""""""""
" Don't use Ex mode, use Q for formatting
map Q gq

"""""""""""""""""""""""""""""""""""""""""""""""""
" allow tab/shift-tab of selected sections
map <Tab> >0
map <S-Tab> <0
imap <S-Tab> <Esc> < i

" CTRL-U in insert mode deletes a lot.  Use CTRL-G u to first break undo,
" so that you can undo CTRL-U after inserting a line break.
inoremap <C-U> <C-G>u<C-U>

"""""""""""""""""""""""""""""""""""""""""""""""""
" Restore cursor to previous position in a file
function! ResCur()
	if line("'\"") <= line("$")
		normal! g`"
		return 1
	endif
endfunction

augroup resCur
	autocmd!
	autocmd BufWinEnter * call ResCur()
augroup END


"""""""""""""""""""""""""""""""""""""""""""""""""
" Convenient command to see the difference between the current buffer and the
" file it was loaded from, thus the changes you made.
" Only define it when not defined already.
if !exists(":DiffOrig")
	command DiffOrig vert new | set bt=nofile | r ++edit # | 0d_ | diffthis
				\ | wincmd p | diffthis
endif


" don't wrap long lines, but set the bottom scroll bar
" TODO this might be gvim specific
set nowrap guioptions+=b

"""""""""""""""""""""""""""""""""""""""""""""""""
" Search recursively for files from "here"
set path +=**
" and search the Mercurial directory above if it exists
set path +=../.hg**

init.lua

-- neovim settings
-- relies on neovim defaults which are different than vim

vim.opt.backup = false		-- do not keep a backup file, use versions instead
vim.opt.writebackup = false	-- dont want a backup file while editing
vim.opt.ruler = true		-- show the cursor position all the time
vim.opt.showcmd = true		-- display incomplete commands
vim.opt.title = true		-- change the window title to be the file name
vim.opt.modeline = true
vim.opt.cscopetagorder = 0	-- search cscope database before tags
vim.opt.cscopetag = true	-- search cscope database for tags
vim.opt.cscopeverbose = false
vim.opt.termguicolors = true
vim.opt.guicursor = ''
vim.opt.background = 'light'
vim.opt.mouse = 'a'

vim.opt.clipboard = 'unnamedplus'

--------------------------------------------------
-- Search recursively for files from "here"
vim.opt.path:append('**')
-- and search the Mercurial directory above if it exists
vim.opt.path:append('../.hg**')

--------------------------------------------------
-- Use all available colors for highlight (mark.vim)
vim.g.mwDefaultHighlightingPalette = 'maximum'

--------------------------------------------------
-- From the neovim wiki under "clipboard=autoselect is not implemented yet"
-- oh, and xsel doesn't seem to work but xclip does
-- TODO this seems to work with both nvim-0.6 and nvim-0.7 where as
-- vim.keymap.set("v", "<LeftRelease>", '"*ygv', {}) only works in 0.7
vim.api.nvim_set_keymap("v", "<LeftRelease>", '"*ygv', { noremap = true })

--------------------------------------------------
-- Don't use Ex mode, use Q for formatting
vim.api.nvim_set_keymap("", "Q", "gq", {})

--------------------------------------------------
-- allow tab/shift-tab of selected sections
vim.api.nvim_set_keymap("", "<Tab>", ">0", {})
vim.api.nvim_set_keymap("", "<S-Tab>", "<0", {})
vim.api.nvim_set_keymap("i", "<S-Tab> <Esc>", "< i", {})

--------------------------------------------------
-- Restore cursor to previous position in a file
vim.api.nvim_create_autocmd({ "BufWinEnter" }, {
    pattern = { "*" },
    callback = function()
        if vim.fn.line("'\"") <= vim.fn.line("$") then
            vim.api.nvim_exec('silent! normal! g`"zv', false)
        end
    end,
})

--------------------------------------------------
-- All Packer plugins in lua/plugins.lua
require('plugins')

No comments: