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版本有以下几点问题:

  1. API用起来太长——这是最小的问题,可以自己包装下、用表格和循环等解决;
  2. 引号太多——无解;
  3. 无法自动捕获augroupid——无解。

后两者使LuaVim 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

如果我们要在宏里生成新的变量,需要注意两点:

  1. 生成的变量不要和已有变量重名;
  2. 在宏里要记住生成的变量名,别声明的是x1,用的时候又变成了x2。

还好Fennel替我们处理了这两点:在宏里以#结尾的变量名会在生成代码时被替换为不重名的新名字;当我们要把这种变量当参数传参时,只要用backtick quote一下就好。具体使用情况见下面M.augroupgroup#

(λ 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

我只是浅显地接触了一点NeovimFennel。在这里贴出我的配置,只是因为按照文章的发展来看应该贴一下使用效果了。如果你有什么建议,欢迎在评论区留言。

(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配置的过程中,用到了如下插件:

GitHub上还有一些其他人为了配置Neovim而写的Fennel宏。我个人觉得宏像量体裁衣,所以倾向于自己写。不过还是把它们放在这里,毕竟有时也能在商场买到很合身的衣服:


复制以下链接,并粘贴到你的Mastodon、MisskeyGoToSocial等应用的搜索栏中,即可搜到对应本文的嘟文。对嘟文进行的点赞、转发、评论,都会出现在本文底部。快去试试吧!

链接:https://emptystack.top/note/fennel-macro-neovim


一人赞过:
  1. 冥王星爱丽