Skip to content

配置适用于嵌入式开发的 Neovim

· 17 min

在使用了 Neovim 将近一年后, 我决定重新配置我的 Neovim. 我之前使用的是 NvChad, 它是一个非常好用的 Neovim 配置. 但是由于之前只会装插件, 抄别人配置的时候总有一种稀里糊涂的感觉. 现在正好趁着空闲学习一下 Neovim 的配置, 顺便将它打造得更轻量化和适合自己.

此处非常感谢 Fledge 在 B 站上的教程, 我的配置基本上是根据这个教程来的.

开始之前#

Install#

Arch Linux 下直接使用 pacman 安装即可:

Terminal window
sudo pacman -S neovim # or yay -S neovim-git

其他系统的安装方式见官方文档.

如果之前安装过, 请先备份之前的数据:

Terminal window
mv ~/.config/nvim{,.bak}
mv ~/.local/share/nvim{,.bak}
mv ~/.local/state/nvim{,.bak}
mv ~/.cache/nvim{,.bak}

此外, 你可能想要设置 Neovim 为默认编辑器:

~/.zshrc
export VISUAL=nvim
export EDITOR=nvim

使用 :checkhealth 检查安装状态.

Neovim 与 Lua#

Neovim 使用 Lua 作为配置文件的语言. 在开始配置 Neovim 之前, 你可能需要先学习一下 Lua 基本语法. 我的 Wiki 中有一个基础的教程.

你可以从 Neovim 的命令行运行 Lua 代码, 每个 :lua 命令都有自己的作用域.

:lua print("Hello")
:lua ="Hello"
:lua =package
:source ~/programs/baz/myluafile.lua
:source # current buffer

Neovim 的配置文件位于 ~/.config/nvim/init.lua, 在这个文件中 require 其他的 module. 在 Neovim 中,lua 目录自动包含在搜索路径 package.path 中. 模块名对应文件名的规则如下:

require("mod") -- lua/mod.lua or lua/mod/init.lua

基础配置#

首先配置一些基础的选项, 在 init.lua 中添加如下语句即可. 使用 :help <option> 获取它们的含义.

local option = vim.opt
local buffer = vim.b
local global = vim.g
-- Hint: use `:h <option>` to figure out the meaning if needed
-- ui
option.number = true -- show absolute number
option.relativenumber = true -- add numbers to each line on the left side
option.cursorline = true -- highlight cursor line underneath the cursor horizontally
option.splitbelow = true -- open new vertical split bottom
option.splitright = true -- open new horizontal splits right
option.termguicolors = true -- enable 24-bit RGB color in the TUI
option.showmode = false -- we are experienced, wo don't need the "-- INSERT --" mode hint
option.wrap = false
-- tab
option.tabstop = 4 -- number of visual spaces per TAB
option.softtabstop = 4 -- number of spacesin tab when editing
option.shiftwidth = 4 -- insert 4 spaces on a tab
option.expandtab = true -- tabs are spaces, mainly because of python
option.shiftround = true
option.autoindent = true
option.smartindent = true
-- search
option.incsearch = true -- search as characters are entered
option.hlsearch = true -- do not highlight matches
option.ignorecase = true -- ignore case in searches by default
option.smartcase = true -- but make it case sensitive if an uppercase is entered
-- other
option.clipboard = 'unnamedplus' -- use system clipboard
option.completeopt = { 'menuone', 'noselect' }
option.mouse = 'a' -- allow the mouse to be used in Nvim
option.exrc = true -- .nvim.lua
option.wildmenu = true
option.backspace = { "indent", "eol", "start" }
option.signcolumn = "yes"
option.autoread = true
option.title = true
option.updatetime = 50
option.swapfile = false
option.backup = false
option.undofile = true
option.undodir = vim.fn.expand('$HOME/.local/share/nvim/undo')
-- buffer
buffer.fileenconding = "utf-8"
-- global
global.mapleader = " "
global.maplocalleader = " "

这里有几个有意思的配置项:

配置按键映射也是类似的, 调用 api 即可. 以下是一些我个人喜好的按键映射.

local map = vim.keymap.set
-- general
map({ "n", "v" }, "q", "b", { desc = "previous word" })
map("n", "<leader>w", "<cmd> w <cr>", { desc = "write" })
map("n", "<leader>q", "<cmd> q <cr>", { desc = "quit" })
-- clear search highlights
map("n", "<Esc>", "<cmd>noh<CR>", { desc = "general clear highlights" })
-- move line(s)
map("n", "<M-j>", "<cmd> move +1 <cr>", { desc = "move the line down" })
map("n", "<M-k>", "<cmd> move -2 <cr>", { desc = "move the line up" })
map("v", "<M-j>", ":m '>+1<CR>gv=gv", { desc = "move the lines down", silent = true })
map("v", "<M-k>", ":m '<-2<CR>gv=gv", { desc = "move the lines up", silent = true })
-- buffer
map("n", "<Tab>", "<cmd>bnext<CR>")
map("n", "<S-Tab>", "<cmd>bNext<CR>")
map("n", "<leader>x", "<cmd>bd<CR>")
map("n", "<M-Tab>", "<cmd> tabnext <cr>", { desc = "next [Tab]" })
map("n", "<M-n>", "<cmd> tabnew <cr>", { desc = "[n]ew tab" })
-- CTRL - <h j k l>
map("i", "<C-h>", "<Left>", { desc = "move left" })
map("i", "<C-j>", "<Down>", { desc = "move down" })
map("i", "<C-k>", "<Up>", { desc = "move up" })
map("i", "<C-l>", "<Right>", { desc = "move right" })
map("n", "<C-h>", "<C-w>h", { desc = "switch window left" })
map("n", "<C-l>", "<C-w>l", { desc = "switch window right" })
map("n", "<C-j>", "<C-w>j", { desc = "switch window down" })
map("n", "<C-k>", "<C-w>k", { desc = "switch window up" })
-- mini.comment
map("n", "<leader>/", "gcc", { remap = true })
map("v", "<leader>/", "gc", { remap = true })

我们可以将不同类型的配置移到单独的文件中以更好地组织配置文件. 我个人的配置文件组织是这样的:

.
├── init.lua
├── lazy-lock.json
├── lazyvim.json
└── lua
├── config
│   ├── keymap.lua
│   ├── lazy.lua
│   └── option.lua
└── plugin
├── cmp.lua
├── format.lua
├── lsp.lua
├── telescope.lua
├── theme.lua
├── treesitter.lua
├── ui.lua
└── utils.lua

插件安装#

插件管理器 Lazy.nvim#

Neovim 可以使用 :packadd 命令加载插件, 之后便可以使用 require('the_plugin_name').setup 启用和配置在 runtimepath 中的插件了. 但是这些东西自己管理非常麻烦, 于是我们可以使用 Lazy.nvim 管理和配置插件.

在 Lazy.nvim 的官方文档的安装教程中, 有安装引导程序示例. 将其复制到自己的配置文件中, 保存并重新打开 Neovim, Lazy 会自动安装. 下面是安装引导程序的示例:

-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading lazy.nvim so that mappings are correct.
-- This is also a good place to setup other settings (vim.opt)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
-- Setup lazy.nvim
require("lazy").setup({
spec = {
-- import your plugins
{ import = "plugins" },
},
-- Configure any other settings here. See the documentation for more details.
-- colorscheme that will be used when installing plugins.
install = { colorscheme = { "habamax" } },
-- automatically check for plugin updates
checker = { enabled = true },
})

上面的部分是用来安装 Lazy 的. 下面的 require("lazy").setup() 是一个常见的表达, 用来传递插件的配置和初始化插件. 配置的表 (即 setup() 的参数) 通常可以在插件的文档中找到. Lazy 本身也是一个插件, 它的全部配置可以在这里找到.

用 Lazy.nvim 安装和配置插件#

有了 Lazy 之后, 我们就可以非常方便地安装和配置插件了. 现在来安装一个主题插件, 以 rose-pine 为例, 只需要将下面这一行添加到 spec 中即可:

{ "rose-pine/neovim", name = "rose-pine" }

这个表中前面的字符串实际上是 Github 仓库的地址. 有了它 Lazy 就可以自动安装插件了. 现在再次重启 Neovim, 安装好过后, 命令行输入 :colorscheme rose-pine-moon 就可以切换主题了. 打开 /home/dokee/.local/share/nvim/lazy 文件夹可以看到你的插件被安装在了这里.

和 Lazy 一样, 我们也可以在文档中找到该插件的所有配置选项. 但是现在的插件的配置和加载是有 Lazy 管理的, 不需要我们来 require, 所以想要传递配置参数的话可以在表中添加 opt 参数, 这就相当于 require("the_plugin_name").setup(opts).

{
"rose-pine/neovim",
priority = 1000,
name = "rose-pine",
opts = {
styles = {
bold = true,
italic = true,
transparency = true,
},
-- other...
},
}

只要有 opts 参数, Lazy 就会调用 require('the_plugin_name').setup(opts), 哪怕它是 opts = {}. 现在我们想要让这个主题插件加载时自动调用 :colorscheme rose-pine-moon 该怎么办呢? Lazy 提供了另一种配置方式 config, 它的值可以是插件加载后调用的函数, 因此我们可以这样写:

{
"rose-pine/neovim",
priority = 1000,
name = "rose-pine",
config = function()
require("rose-pine").setup {
styles = {
bold = true,
italic = true,
transparency = true,
},
-- other...
vim.cmd("colorscheme rose-pine-moon")
end,
}

这样就可以做除了传入配置表之外的事情了. 此外, config 的值如果是 true 的话就会直接执行 require("rose-pine").setup({}).

为了更好地组织配置文件, 我们可以将插件表放在 lua/plugins 文件夹中, 在 spec 中添加 { import = "plugins" } 这一行即可让 Lazy 找到该文件夹下的所有插件配置. 关于插件表的其他描述可以在这里找到.

插件推荐#

下面列举了我个人安装的插件:

plugin
├── cmp.lua # 自动补全
│ └── hrsh7th/nvim-cmp
│ ├── hrsh7th/cmp-nvim-lsp
│ ├── hrsh7th/cmp-nvim-lua
│ ├── hrsh7th/cmp-path
│ ├── hrsh7th/cmp-buffer
│ ├── hrsh7th/cmp-cmdline
│ └── saadparwaiz1/cmp_luasnip
│ └── L3MON4D3/LuaSnip
│ └── rafamadriz/friendly-snippets
├── format.lua # format
│ └── stevearc/conform.nvim
├── lsp.lua # LSP
│ neovim/nvim-lspconfig
│ ├── williamboman/mason.nvim # LSP 和 formatter 等的管理插件
│ ├── williamboman/mason-lspconfig
│ ├── folke/lazydev.nvim
│ ├── folke/neoconf.nvim # 可以使用 VSCode 的 json 配置
│ ├── j-hui/fidget.nvim # 右下角显示 LSP 状态
│ ├── nvimdev/lspsaga.nvim # 集成很多方便工具, 不过我只用它的 code action 和浮动终端
│ ├── p00f/clangd_extensions.nvim # clangd 拓展, 见下文
│ └── SmiteshP/nvim-navbuddy # 提供类似 ranger 的方式查看 breadcrumb symbols
│ ├── SmiteshP/nvim-navic
│ └── MunifTanjim/nui.nvim
├── telescope.lua # 文件搜索等
│ nvim-telescope/telescope.nvim
│ ├── nvim-lua/plenary.nvim
│ └── nvim-telescope/telescope-fzf-native.nvim
├── theme.lua
│ rose-pine/neovim
├── treesitter.lua # 代码高亮
│ nvim-treesitter/nvim-treesitter
├── ui.lua
│ ├── nvim-lualine/lualine.nvim # 底部栏
│ │ └── nvim-tree/nvim-web-devicons
│ ├── akinsho/bufferline.nvim # 顶部 buffer 栏
│ │ └── nvim-tree/nvim-web-devicons
│ ├── lukas-reineke/indent-blankline.nvim # 缩进提示线
│ ├── lewis6991/gitsigns.nvim # git 标记
│ ├── folke/noice.nvim # 美化
│ │ ├── rcarriga/nvim-notify
│ │ └── MunifTanjim/nui.nvim
│ ├── https://gitlab.com/HiPhish/rainbow-delimiters.nvim.git # 给不同层级的括号不同颜色
│ └── nvim-zh/colorful-winsep.nvim # 多窗口下突出显示当前窗口
└── utils.lua
├── rainbowhxch/accelerated-jk.nvim # normal 模式下长按 j k 时加速上下移动
├── DanilaMihailov/beacon.nvim # 大距离跳转时突出显示光标
├── folke/persistence.nvim # 快速打开上一次打开的文件
├── windwp/nvim-autopairs # 括号引号等自动配对补全
├── ethanholz/nvim-lastplace # 记录退出文件时的光标位置
├── folke/flash.nvim # 快速移动光标
├── echasnovski/mini.ai # textobject 增强
├── echasnovski/mini.comment # 智能注释
├── max397574/better-escape.nvim # insert 模式下同时按 jk 等效为 <esc>
├── roobert/search-replace.nvim # 更好的搜索替换
├── LeonHeidelbach/trailblazer.nvim # 记录光标位置并跳转
├── wakatime/vim-wakatime # 记录 Neovim 使用时长
└── brenoprata10/nvim-highlight-colors # 显示色号代码对应的颜色
感谢所有为 Neovim 插件做出贡献的人!

配置 clangd#

clangd 常见报错及解决#

有关 LSP 等的配置过于复杂, 这里不再赘述. 对于嵌入式设备通常使用 GCC, 但是 GCC 对 LSP 没有支持, 因此 clangd 几乎是唯一的选择. 也有一些常使用 clang 的, 比如 Espressif, 但 GCC 还是多数. 现在 Arm 也有了对 clang 工具链的支持, 但用的人比较少.

混合使用 GCC 和 clangd 可能遇到很多问题, 很不幸的是这些问题暂时可能还没有解决方案, 可以参见 clangd 在 Github 上的 issue.

以一个 STM32 项目为例. 这个项目使用 arm-none-eabi-gccarm-none-eabi-g++ 编译. 在项目能正常编译的情况下, 使用 clangd 却可能出现很多报错.

一个常见的错误可能是 clangd 找不到 (标准库的) 头文件. 根据 clangd 官网的指导, clangd 的正确解析需要在项目的根目录下提供 compile_commands.json 文件, 如果使用 make 可以通过 bear -- make 来获得, 如果使用 cmake 则需要 set(CMAKE_EXPORT_COMPILE_COMMANDS ON). 一般来说此时的头文件就都可以正确识别了.

此时可能还有标准库头文件找不到 (或使用了错误的标准库) 的情况. 官网有对这种情况的说明. 首先, 检查类似下面命令的输出:

Terminal window
arm-none-eabi-gcc -v -c -xc++ /dev/null

在它的输出中可以找到标准库的路径. 然后你可以使用下面的命令来检查它是否正确识别到了 compile_commands.json:

Terminal window
clangd --check=/path/to/a/file/in/your/project.cc [--log=verbose]

一般来说 clangd 会根据 compile_commands.json 中的命令来获得标准库, 如果它正确读取了 compile_commands.json 文件但仍然出错的话, 你可以手动为将 flag -isystem 添加到编译命令中. 创建 .clangd 文件, 添加下面的语句:

CompileFlags:
Add: [
-isystem,
/usr/lib/gcc/arm-none-eabi/14.2.0/../../../../arm-none-eabi/include/c++/14.2.0,
-isystem,
/usr/lib/gcc/arm-none-eabi/14.2.0/../../../../arm-none-eabi/include/c++/14.2.0/arm-none-eabi,
-isystem,
/usr/lib/gcc/arm-none-eabi/14.2.0/../../../../arm-none-eabi/include/c++/14.2.0/backward,
-isystem,
/usr/lib/gcc/arm-none-eabi/14.2.0/include,
-isystem,
/usr/lib/gcc/arm-none-eabi/14.2.0/include-fixed,
-isystem,
/usr/lib/gcc/arm-none-eabi/14.2.0/../../../../arm-none-eabi/include
]

现在 clangd 应该就可以正确找到标准库了. 另外一种方法是为 clangd 命令添加 --query-driver="/usr/bin/arm-none-eabi-gcc,/usr/bin/arm-none-eabi-g++" 这样的参数, 你可以使用下面的命令测试它是否正常工作:

Terminal window
clangd --query-driver="/usr/bin/arm-none-eabi-gcc,/usr/bin/arm-none-eabi-g++" --check=/path/to/a/file/in/your/project.cc [--log=verbose]

另一个错误是由于 clangd 实际使用的是内置的 clang parser (cc1), 因此它无法识别 GCC 的内置宏 (如 __GNUC__). 很遗憾, 这个问题暂时没有太好的解决方法 (见 clangd 的 issue #533 等). 我遇到的问题是在 STM32 项目的 cmsis_compiler.h 中由于 clangd 识别 __clang__ 宏导致找不到头文件 cmsis_clang.hm-profile/cmsis_clang_m.h. 解决方法只是将这两个头文件手动复制进去.

还有一种可能的错误是使用了 clang 不兼容的语法, 这个解决方法只能是修改代码了.

插件: clangd_extensions.nvim#

这个插件可以提供许多对于 C/C++ 方便的功能:

我目前只用了前三个功能, 下面分享我个人的插件配置, 它放在 clangd 的 on_attach 中.

vim.keymap.set('n', 'L', '<cmd>ClangdSymbolInfo<cr>', { buffer = bufnr, silent = true })
vim.keymap.set('n', 'H', '<cmd>ClangdSwitchSourceHeader<cr>', { buffer = bufnr, silent = true })
require("clangd_extensions").setup {
inlay_hints = {
inline = false;
max_len_align = true;
parameter_hints_prefix = ' ',
other_hints_prefix = '󰊰 ',
},
memory_usage = {
border = "rounded",
},
symbol_info = {
border = "rounded",
},
}
require("clangd_extensions.inlay_hints").setup_autocmd()
require("clangd_extensions.inlay_hints").set_inlay_hints()

Reference#