Windows 下使用 Neovim

虽然在 Windows 下的 VSCode 用得挺好,但还是想折腾一下 Neovim 试试。Neovim 安装完毕后只是一个简陋的编辑器,需要自己配置和安装各种插件才能进行日常使用,幸好已经有很多现成的开源配置项目,像 LunarVimAstroNvimLazyVim 等。经过试用,发现 AstroNvim 在 Windows 上有一些未解决的 BUG ,比如一直报「 E65: Illegal back reference 」错误。 LunarVim 暂时还没机会测试。暂且选择使用 LazyVim 的配置。

此外,由于 Neovim 是一个命令行程序,需要搭配支持真彩色和 underurl 的命令行终端使用。 LazyVim 官方推荐了几款终端,其中 weztermalacritty 支持 Windows 平台。另外,微软的 Windows Terminal 也勉强可用。不过这里推荐使用 Neovim 的专属前端 GUI —— Neovide

本文所有软件和工具都使用 Scoop 进行安装。如何使用 Scoop 可以参考之前的文章《Windows 包管理工具》。由于众所周知的原因,安装前需要配置科学上网(根据自己的网络环境修改代理服务器地址):

POWERSHELL$env:http_proxy='http://127.0.0.1:7890'
$env:https_proxy='http://127.0.0.1:7890'
scoop config proxy 127.0.0.1:7890

安装

安装必要组件:

POWERSHELLscoop install neovim neovide git lazygit gcc ripgrep fd unzip tree-sitter luarocks 

为 Git 访问 GitHub 设置代理:

POWERSHELLgit config --global http.https://github.com.proxy socks5://127.0.0.1:7890

下载并安装 LazyVim 配置:

POWERSHELLgit clone https://github.com/LazyVim/starter $env:LOCALAPPDATA\nvim --depth=1

为了显示 Neovim 界面中的图标,需要安装一款 Nerd Font 字体,这里选择 UbuntuMono Nerd Font 字体。可以通过 Scoop 进行安装:

POWERSHELLscoop bucket add nerd-fonts
scoop install UbuntuMono-NF

在配置文件 $env:LOCALAPPDATA\nvim\lua\config\options.lua 中添加下行代码来设置字体:

LUAlocal opt = vim.opt

opt.guifont = "UbuntuMono Nerd Font:h12"

在命令行中使用 neovide 命令就可以启动 Neovim 了。值得注意的是, Neovide 只要使用参数 --wsl 就可以在 WSL 环境中运行,不过这时 LazyVim 的配置也需要安装到 WSL 环境中:

SHELLgit clone https://github.com/LazyVim/starter ~/.config/nvim --depth=1

初始化

程序第一次运行时会自动下载和安装插件。如果遇到网络问题而导致安装失败也没关系,按下 R (大写)重新安装直至成功为止。所有插件安装成功后, nvim-treesitter 还会继续安装组件,等所有插件和组件安装完毕后,按下 q 退出安装界面。

LazyVim 初始化安装

意外的是,在 nvim-treesitter 安装 regex 组件时遇到了奇怪的错误:

nvim-treesitter-regex-error

这是 Git 报的错,原因是 tree-sitter-regex 仓库 checkout 了一个无效的 commit ID 。这应该是 nvim-treesitter 的一个 BUG 。

打开 %LOCALAPPDATA%\nvim-data\lazy\nvim-treesitter\lockfile.json 文件,定位到第 438 行:

JSON   "regex": {
     "revision": "17a3293714312c691ef14217f60593a3d093381c"
   },

revision 的值改为 2354482d7e2e8f8ff33c1ef6c8aa5690410fbc96 并保存,运行命令 :TSUpdate all 强制更新。

2023 年 7 月 24 日,最新版本的 nvim-treesitter 已经修复了这个错误。

基础操作

Neovim 主要通过键盘操作,记住这些操作键是比较令人头大的事情。还好 LazyVim 安装了 which-key.nvim 插件来帮助用户记住按键映射。

按下空格键呼出主菜单,用户可以按照菜单中的提示进行操作。比如打开文件浏览侧边栏对应的快捷键是 E ,退出的快捷键是 q 。需要注意的是,Neovim 的所有的操作按键都是区分大小写的。

主菜单

详细 Keymaps 可以参考官方文档:https://www.lazyvim.org/keymaps (表格中的 <leader> 对应的是空格键)。

一些操作会在当前界面里弹出窗口,这些窗口其中一部分可以通过按键 q 关闭,而另一些需要按键 <esc> 关闭。比如 <leader>l 呼出的 Lazy 窗口,可以按 q 键退出(也可以通过 <leader>wd 退出);而 <leader><space> 呼出的搜索窗口,则需要按两下 <esc> 键来退出(第一次按 <esc> 键让文本输入框从插入模式返回到正常模式)。

用户可以在界面的左下角查看当前编辑窗口所处的模式。在 NORMAL 模式下,快捷键才能起作用。不同模式的作用可参见官方文档:https://neovim.io/doc/user/intro.html#vim-modes-intro

编辑窗口 NORMAL 模式下的常用快捷键:

按键操作
i切换到插入模式
:切换到命令模式
h左移一个字符
j下移一个字符
k上移一个字符
l右移一个字符
0移至行首
$移至行尾
^移至本行第一个非空字符
w向右移动一个单词
W向右移动一个单词(以空格分隔)
2w向右移动两个单词,2 可以替换成其他数字
2W向右移动两个单词(以空格分隔),2 可以替换成其他数字
b向左移动一个单词
B向左移动一个单词(以空格分隔)
2b向左移动 2 个单词,2 可以替换成其他数字
2B向左移动 2 个单词(以空格分隔),2 可以替换成其他数字
G移至文档末尾
gg移至文档首行
a光标后插入
A移至行末插入
i光标前插入
o光标下插入一行
O光标上插入一行
x删除光标处字符
dw删除一个词
d0删至行首
d$删至行末
d)删至句末
dgg删至文件开头
dG删至文件末尾
dd删除该行
2dd删除两行,2 可以替换成其他数字
r替换当前字符
R切换到 REPLACE 模式
u撤回操作
<C-r>重做撤回的操作
yy复制当前行
p在当前行之后粘贴存储缓冲区中的内容
P在当前行之前粘贴存储缓冲区中的内容
v打开 VISUAL 模式菜单
V切换到逐行选择的 VISUAL 模式
/向后搜索
?向前搜索
n下一个搜索结果
N上一个搜索结果
ggoto 菜单
gd跳转到定义
gcc注释改行
<C-w>window 菜单
<C-w>s水平拆分窗口
<C-w>v垂直拆分窗口
<C-q>V-BLOCK 模式

编辑窗口 VISUAL 模式下的常用快捷键:

按键操作
~切换大小写
d删除
c变更
y复制
>增加缩进
<减少缩进

通过快捷键 <leader>E<leader>e 可以在侧边栏打开 neo-tree.nvim 的文件浏览窗口。该窗口的操作快捷键如下:

按键操作
j下移
k上移
<cr>打开
a新建文件或目录
A新建目录
d删除
r重命名
c复制
m移动
q关闭窗口

常用命令

Neovim 在命令模式下,可以按下 : 来输入命令。

输入命令

:checkhealth

该命令用于检查 Neovim 的工作状态,可以根据结果提示来安装缺少的组件。

==============================================================================
lazy: require("lazy.health").check()

lazy.nvim ~
- OK Git installed
- OK no existing packages found by other package managers
- OK packer_compiled.lua not found
- WARNING {flash.nvim}: unknown key <vscode>

==============================================================================
lazyvim: require("lazyvim.health").check()

LazyVim ~
- OK Using Neovim >= 0.8.0
- OK `git` is installed
- OK `rg` is installed
- OK `fd` is installed
- OK `lazygit` is installed

==============================================================================
noice: require("noice.health").check()

noice.nvim ~
- OK **Neovim** >= 0.8.0
- OK Not running inside **Neovide**
- OK You're using a GUI that should work ok
- OK **vim.go.lazyredraw** is not enabled
- OK **nvim-notify** is installed
- OK **TreeSitter vim** parser is installed
- OK **TreeSitter regex** parser is installed
- OK **TreeSitter lua** parser is installed
- OK **TreeSitter bash** parser is installed
- OK **TreeSitter markdown** parser is installed
- OK **TreeSitter markdown_inline** parser is installed
- OK `vim.notify` is set to **Noice**
- OK `vim.lsp.handlers["textDocument/hover"]` is set to **Noice**
- OK `vim.lsp.handlers["textDocument/signatureHelp"]` is set to **Noice**
- OK `vim.lsp.handlers["window/showMessage"]` is set to **Noice**
- OK `vim.lsp.util.convert_input_to_markdown_lines` is set to **Noice**
- OK `vim.lsp.util.stylize_markdown` is set to **Noice**

==============================================================================
nvim: require("nvim.health").check()

Configuration ~
- OK no issues found

Runtime ~
- OK $VIMRUNTIME: D:\Apps\scoop\local\apps\neovim\current\share\nvim\runtime

Performance ~
- OK Build type: RelWithDebInfo

Remote Plugins ~
- OK Up to date

==============================================================================
nvim-treesitter: require("nvim-treesitter.health").check()

Installation ~
- OK `tree-sitter` found 0.20.8 (0c49d6745b3fc4822ab02e0018770cd6383a779c) (parser generator, only needed for :TSInstallFromGrammar)
- OK `node` found v20.4.0 (only needed for :TSInstallFromGrammar)
- OK `git` executable found.
- OK `gcc` executable found. Selected from { vim.NIL, "cc", "gcc", "clang", "cl", "zig" }
  Version: gcc (MinGW-W64 x86_64-msvcrt-posix-seh, built by Brecht Sanders) 13.1.0
- OK Neovim was compiled with tree-sitter runtime ABI version 14 (required >=13). Parsers must be compatible with runtime ABI.

OS Info:
{
  machine = "x86_64",
  release = "10.0.19045",
  sysname = "Windows_NT",
  version = "Windows 10 IoT Enterprise"
} ~

Parser/Features         H L F I J
  - bash                ✓ ✓ ✓ . ✓
  - c                   ✓ ✓ ✓ ✓ ✓
  - html                ✓ ✓ ✓ ✓ ✓
  - javascript          ✓ ✓ ✓ ✓ ✓
  - json                ✓ ✓ ✓ ✓ .
  - lua                 ✓ ✓ ✓ ✓ ✓
  - luadoc              ✓ . . . .
  - luap                ✓ . . . .
  - markdown            ✓ . ✓ ✓ ✓
  - markdown_inline     ✓ . . . ✓
  - php                 ✓ ✓ ✓ ✓ ✓
  - python              ✓ ✓ ✓ ✓ ✓
  - query               ✓ ✓ ✓ ✓ ✓
  - regex               ✓ . . . .
  - tsx                 ✓ ✓ ✓ ✓ ✓
  - typescript          ✓ ✓ ✓ ✓ ✓
  - vim                 ✓ ✓ ✓ . ✓
  - vimdoc              ✓ . . . ✓
  - yaml                ✓ ✓ ✓ ✓ ✓

  Legend: H[ighlight], L[ocals], F[olds], I[ndents], In[j]ections
         +) multiple parsers found, only one will be used
         x) errors found in the query, try to run :TSUpdate {lang} ~

==============================================================================
provider: health#provider#check

Clipboard (optional) ~
- OK Clipboard tool found: win32yank

Python 3 provider (optional) ~
- WARNING No Python executable found that can `import neovim`. Using the first available executable for diagnostics.
- WARNING Could not load Python 3:
  D:\Apps\scoop\local\shims\python3.EXE does not have the "neovim" module.
  python3.10 not found in search path or not executable.
  python3.9 not found in search path or not executable.
  python3.8 not found in search path or not executable.
  python3.7 not found in search path or not executable.
  D:\Apps\scoop\local\apps\python\current\python.EXE does not have the "neovim" module.
  - ADVICE:
    - See :help |provider-python| for more information.
    - You may disable this provider (and warning) by adding `let g:loaded_python3_provider = 0` to your init.vim
- Executable: Not found

Python virtualenv ~
- OK no $VIRTUAL_ENV

Ruby provider (optional) ~
- WARNING `ruby` and `gem` must be in $PATH.
  - ADVICE:
    - Install Ruby and verify that `ruby` and `gem` commands work.

Node.js provider (optional) ~
- Node.js: v20.4.0
- WARNING Missing "neovim" npm (or yarn, pnpm) package.
  - ADVICE:
    - Run in shell: npm install -g neovim
    - Run in shell (if you use yarn): yarn global add neovim
    - Run in shell (if you use pnpm): pnpm install -g neovim
    - You may disable this provider (and warning) by adding `let g:loaded_node_provider = 0` to your init.vim

Perl provider (optional) ~
- WARNING No usable perl executable found

==============================================================================
vim.lsp: require("vim.lsp.health").check()

- LSP log level : WARN
- Log path: C:\Users\fournoas\AppData\Local\nvim-data\lsp.log
- Log size: 0 KB

vim.lsp: Active Clients ~
- No active clients

==============================================================================
vim.treesitter: require("vim.treesitter.health").check()

- Nvim runtime ABI version: 14
- OK Parser: bash       ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\bash.so
- OK Parser: c          ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\c.so
- OK Parser: html       ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\html.so
- OK Parser: javascript ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\javascript.so
- OK Parser: json       ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\json.so
- OK Parser: lua        ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\lua.so
- OK Parser: luadoc     ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\luadoc.so
- OK Parser: luap       ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\luap.so
- OK Parser: markdown   ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\markdown.so
- OK Parser: markdown_inline ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\markdown_inline.so
- OK Parser: php        ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\php.so
- OK Parser: python     ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\python.so
- OK Parser: query      ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\query.so
- OK Parser: regex      ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\regex.so
- OK Parser: tsx        ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\tsx.so
- OK Parser: typescript ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\typescript.so
- OK Parser: vim        ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\vim.so
- OK Parser: vimdoc     ABI: 14, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\vimdoc.so
- OK Parser: yaml       ABI: 13, path: C:\Users\fournoas\AppData\Local\nvim-data\lazy\nvim-treesitter\parser\yaml.so
- OK Parser: c          ABI: 14, path: D:\Apps\scoop\local\apps\neovim\current\lib\nvim\parser\c.dll
- OK Parser: lua        ABI: 14, path: D:\Apps\scoop\local\apps\neovim\current\lib\nvim\parser\lua.dll
- OK Parser: query      ABI: 14, path: D:\Apps\scoop\local\apps\neovim\current\lib\nvim\parser\query.dll
- OK Parser: vim        ABI: 14, path: D:\Apps\scoop\local\apps\neovim\current\lib\nvim\parser\vim.dll
- OK Parser: vimdoc     ABI: 14, path: D:\Apps\scoop\local\apps\neovim\current\lib\nvim\parser\vimdoc.dll

==============================================================================
which-key: require("which-key.health").check()

WhichKey: checking conflicting keymaps ~
- WARNING conflicting keymap exists for mode **"n"**, lhs: **"gc"**
- rhs: ` `

:TSInstall

这是由 nvim-treesitter 提供的命令,用于安装语言解析器以支持语法高亮特性。比如可以用 :TSInstall css 命令来安装 CSS 的解析器。通过 :TSInstallInfo 命令可以查看支持的语言列表和当前的安装状态。通过 :TSUpdate 命令可以手动更新解析器。

TSInstallInfo

:Mason

这是由 mason.nvim 提供的命令,用于呼出 mason.vim 的管理界面。Mason 用来管理 LSPDAP 等组件。运行命令 :Mason 打开配置界面,按 2 进入 LSP 配置页面。如果需要安装 Python 的 LSP ,按下 ctrl + f ,输入 python 然后回车,可以看到所有的 Python LSP ,选中其中的 pyright ,按下 i 进行安装( BTW :按 X 卸载)。

Mason

:terminal

新建一个命令行窗口。也可以用缩写 :te

个性化定制

保存在 %LOCALAPPDATA%\nvim 目录下的配置其实就是 LazyVim 自定义模板,可以直接在配置中进行修改,也可以在 %LOCALAPPDATA%\nvim\lua\plugins 目录下新建配置文件。

比如,要让 Neovim 支持 PHP 开发环境,可以新建 %LOCALAPPDATA%\nvim\lua\plugins\php.lua 文件,内容如下:

LUAreturn {
  {
    "nvim-treesitter/nvim-treesitter",
    opts = function(_, opts)
      vim.list_extend(opts.ensure_installed, {
        "php",
        "phpdoc",
        "javascript",
        "html",
        "css",
        "scss",
      })
    end,
  },

  {
    "williamboman/mason.nvim",
    opts = function(_, opts)
      vim.list_extend(opts.ensure_installed, {
        "html-lsp",
        "css-lsp",
      })
    end,
  },

  {
    "neovim/nvim-lspconfig",
    opts = {
      servers = {
        intelephense = {},
      },
    },
  },

}

在重启 Neovim 后,会自动安装插件。

%LOCALAPPDATA%\nvim\lua\config 目录下有三个配置文件,分别是:

比如,新建快捷键 q 映射为 delete buffer 操作,可以在 keymaps.lua 中添加内容:

LUAlocal keymap = vim.keymap.set

keymap('n', 'q', function() require("mini.bufremove").delete(0, true) end, { desc = 'delete buffer' })

options.lua 中添加以下内容,用于禁用 relativenumber 行号,以及将 Powershell 设为默认 Shell :

LUAopt.relativenumber = false
opt.shell = "pwsh -nologo"

Neovide 启动后,输入法会处于激活状态,导致输入命令前需要先切换到英文输入状态。可以添加配置,默认关闭输入法,仅在插入模式、命令模式以及终端模式下才能启用输入法。

编辑 %LOCALAPPDATA%\nvim\lua\config\options.lua 文件,添加如下代码:

LUAlocal glob = vim.g

if glob.neovide then
  glob.neovide_input_ime = false
end

编辑 %LOCALAPPDATA%\nvim\lua\config\keymaps.lua 文件,添加如下代码:

LUA-- 在 INSERT, COMMAND, TERMINAL 模式下,按下 <Shift+Space> 开启或关闭输入法
keymap({'c', 'i', 't'}, '<S-Space>', function() vim.g.neovide_input_ime = not vim.g.neovide_input_ime end, { silent = true })

编辑 %LOCALAPPDATA%\nvim\lua\config\autocmds.lua 文件,添加如下代码:

LUAlocal ime_input = vim.api.nvim_create_augroup("ime_input", { clear = true })

-- 模式切换后关闭输入法
vim.api.nvim_create_autocmd({ "ModeChanged" }, {
    group = ime_input,
    pattern = "*",
    callback = function() vim.g.neovide_input_ime = false end
})