用Fennel宏配置Neovim
我的Vim、Neovim共用一套Vim Script配置(不同之处用模板生成)。现在Neovim有很多用Lua编写的插件需要用Lua配置,同时我又不喜欢在Vim Script里嵌入字符串/HEREDOC形式的Lua代码,所以我开始把我的Vim Script翻译成Lua代码,然而:
Lua太繁琐
假设我们有下面的Vim Script配置:
tnoremap <Esc> <C-\><C-n>
let mapleader = " "
set ignorecase smartcase
set matchpairs+=<:>
augroup BFG
autocmd!
autocmd FileType c,cpp,verilog setlocal commentstring=//%s
autocmd BufWritePre * %s/\s\+$//e
augroup END
翻译成Lua以后竟然是:
vim.keymap.set("t", "<Esc>", [[<C-\><C-n>]])
vim.g.mapleader = " "
vim.o.ignorecase = true
vim.o.smartcase = true
vim.opt.matchpairs:append({"<:>"})
local group = vim.api.nvim_create_augroup("BFG", {clear = true})
vim.api.nvim_create_autocmd("FileType", {
group = group,
pattern = {"c", "cpp", "verilog"},
callback = function() vim.bo.commentstring = "//%s",
})
vim.api.nvim_create_autocmd("BufWritePre", {
group = group,
command = [[%s/\s\+$//e]],
})
Lua版本有以下几点问题:
- API用起来太长——这是最小的问题,可以自己包装下、用表格和循环等解决;
- 引号太多——无解;
- 无法自动捕获augroup的id——无解。
后两者使Lua比Vim Script冗长许多,但它们貌似是部分人相比Vim Script更喜欢Lua的原因——Lua更像通用编程语言。写插件时确实需要Lua这样的通用语言,但只是用Lua操纵一些选项、创建一些自动命令,实在有些笨重了。怪不得有人的Lua配置里哪哪都是vim.cmd [[xxxx]]
。
Fennel,快用你无敌的宏来想想办法
Fennel是披着Lisp皮的Lua。说它披着Lisp皮,是因为它拥有把代码视作列表,通过修改列表返回新代码的宏;说它其实是Lua,是因为它不仅可以被翻译为Lua,写起来也像Lua。
拜拜了引号
我第一次使用Fennel,是在参加Lisp Game Jam 2022时。那时只把它当带括号的Lua写,没怎么用它的宏。这次写Neovim配置倒是让它的宏大显身手的好时机,因为宏可以帮你自动加双引号:
>> (macro dq [x] (tostring x))
nil
>> (dq x)
"x"
Fennel的宏展开是在翻译时发生的,所以在写Fennel代码时可以直接写x,而在输出的Lua代码里得到"x"。
假如我们在macros.fnl中有如下代码:
(local M {})
(local fn? #(or (list? $) (multi-sym? $)))
(λ to-string/fn [x]
"Call tostring only if x isn't a function.
Normal function can survive after tostring, but hashfn can't."
(if (fn? x) x (tostring x)))
(λ set-keymap [mode]
"Base function for *noremap"
#(let [lhs (tostring $1)
rhs (to-string/fn $2)]
`(vim.keymap.set ,mode ,lhs ,rhs ,$3)))
(set M.cnoremap (set-keymap :c))
M
那在init.fnl中导入cnoremap
后就可以用(tnoremap <Esc> <C-\><C-n>)
来生成vim.keymap.set("t", "<Esc>", "<C-\\><C-n>")
。不过这有个小缺陷:像“:
”、“;
”这种对Fennel parser有特殊意义的字符还是需要手动加引号。
注意set-keymap里最终返回的`(vim.keymap.set
,mode ,lhs ,rhs
,$3)
,它就是我们要通过宏生成的代码的样子。用backtick开头的括号会被保留原样而不当作函数执行,内部逗号开头的表达式又会被替换成执行结果。
自动捕获group id
如果我们要在宏里生成新的变量,需要注意两点:
- 生成的变量不要和已有变量重名;
- 在宏里要记住生成的变量名,别声明的是x1,用的时候又变成了x2。
还好Fennel替我们处理了这两点:在宏里以#结尾的变量名会在生成代码时被替换为不重名的新名字;当我们要把这种变量当参数传参时,只要用backtick quote一下就好。具体使用情况见下面M.augroup
的group#
:
(λ apply-seq-or-single [f x]
"For autocmd's event & pattern, generalised"
(if (sequence? x)
(icollect [_ y (ipairs x)]
(f y))
(f x)))
(λ tostring-seq-or-single [x]
"For autocmd's event & pattern"
(apply-seq-or-single tostring x))
(λ gen-autocmds [group xs]
"Generate a list of nvim_create_autocmd within group"
(icollect [_ autocmd (ipairs xs)]
(let [event (tostring-seq-or-single (. autocmd 2))
pattern (tostring-seq-or-single (. autocmd 3))
once (= (sym :++once) (. autocmd 4))
fn-or-cmd (. autocmd (if once 5 4))
fn-or-cmd-key (if (fn? fn-or-cmd) :callback :command)
opts {: group : pattern : once fn-or-cmd-key (to-string/fn fn-or-cmd)}]
`(vim.api.nvim_create_autocmd ,event ,opts))))
(λ M.augroup [...]
"Call nvim_create_autocmd with automatic group"
(let [name (tostring (select 1 ...))
autocmds [(select 2 ...)]]
`(let [group# (vim.api.nvim_create_augroup ,name {:clear true})]
,(gen-autocmds `group# autocmds))))
然后我们就能用如下Fennel代码去生成Lua那特别死板的代码:
(with-augroup BFG
(autocmd FileType [c cpp verilog] #(set vim.bo.commentstring "// %s"))
(autocmd BufWritePre "*" %s/\s\+$//e))
init.fnl
我只是浅显地接触了一点Neovim和Fennel。在这里贴出我的配置,只是因为按照文章的发展来看应该贴一下使用效果了。如果你有什么建议,欢迎在评论区留言。
(import-macros {: noremap
: nnoremap
: cnoremap
: tnoremap
:let let=
: turn-on
:set set=
: set^
: set+
: set-
:augroup with-augroup} :macros)
; For table
(local expr true)
(local silent true)
; [leader key]
; Must define it before telescope config
(let= mapleader " " maplocalleader " ")
(nnoremap <Leader><CR> vim.cmd.nohlsearch {: silent})
(nnoremap <Leader>n vim.cmd.relativenumber! {: silent})
; set splitbelow splitright
(nnoremap <Leader>w/ vim.cmd.vsplit)
(nnoremap <Leader>w- vim.cmd.split)
(set= splitkeep screen)
; [status line & command line]
; TODO convert to cnoreabbrev
(cnoremap "%%" #(if (= (vim.fn.getcmdtype) ":") (.. (vim.fn.expand "%:h") "/")
"%%") {: expr :desc "Transform %% to $PWD/"})
; Use ; to enter command line mode, : to repeat f/t -- or just F/T,,,,,
(noremap ":" ";")
(noremap ";" ":")
; ! -> :!
(nnoremap ! ":!")
; [terminal]
(tnoremap <Esc> <C-\><C-n>)
(tnoremap <C-v><Esc> <Esc>)
; [misc]
; Plugin settings
; Persistent undo
(turn-on undofile)
; Show lines above and below cursor.
(set= scrolloff 5)
(turn-on number relativenumber)
; Don’t wrap line by default.
; (set= wrap false)
; @see https://stackoverflow.com/a/21000307
(noremap j #(if vim.v.count :gj :j) {: expr : silent})
(noremap k #(if vim.v.count :gk :k) {: expr : silent})
; Only be case sensitive when there are Capital letters.
(turn-on ignorecase smartcase)
; Allow hiding a buffer that has unsaved changes.
(turn-on hidden)
; Disable Ex mode. Use Q to replay last used macro.
(nnoremap Q "@@")
; Enable mouse support.
(set= mouse a)
; Match parenthesis.
(turn-on showmatch)
(set= matchtime 1)
(set+ matchpairs ["<:>"])
; https://github.com/neovim/neovim/issues/4684
(vim.cmd.filetype "plugin indent on")
; [autocmds]
(with-augroup BFG
(autocmd FileType asm #(set vim.bo.commentstring "# %s"))
(autocmd FileType [c cpp verilog] #(set vim.bo.commentstring "// %s"))
(autocmd BufReadPost "*" ; TODO nested?
#(let [jump (= -1
(vim.fn.index [:gitcommit :gitrebase :xxd] vim.o.ft))
marker (vim.fn.line "'\"")
last (vim.fn.line "$")]
(when (and jump (< 1 marker) (<= marker last))
(vim.cmd.normal "g`\""))))
(autocmd BufWritePre "*" %s/\s\+$//e)
(autocmd FileType "*" #(set- formatoptions [:c :r :o]))
(autocmd BufWritePre /tmp/* #(set vim.bo.undofile false)))
; [plugins]
(let [lazypath (.. (vim.fn.stdpath :data) :/lazy/lazy.nvim)]
(when (not (vim.loop.fs_stat lazypath))
(vim.fn.system (.. "git clone --filter=blob:none --branch=stable https://github.com/folke/lazy.nvim.git "
lazypath)))
(set^ rtp lazypath))
(let [lazy (require :lazy)]
(lazy.setup :plugins))
相关插件
我在切换到Fennel配置的过程中,用到了如下插件:
- Olical/nfnl:在保存Fennel代码时自动翻译成Lua代码。没它我根本不想用Fennel配置Neovim,更别提用宏了;
- Olical/conjure:运行选中的Fennel代码,主打一个方便;
- mnacamura/vim-fennel-syntax:没有高亮我可怎么写代码呀;
- nvim-parinfer:Lua版Parinfer,体验和Vim Script版差不多。
GitHub上还有一些其他人为了配置Neovim而写的Fennel宏。我个人觉得宏像量体裁衣,所以倾向于自己写。不过还是把它们放在这里,毕竟有时也能在商场买到很合身的衣服: