Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。

文章代表作者个人观点,少数派仅对标题和排版略作修改。


前言

Vim 绝对是一个让很多人又爱又恨的软件,爱它的速度快与功能强大,恨它的学习曲线与复杂的配置。我接触 Vim 的这些年来,从原版 Vim 折腾到 Neovim,也慢慢构建了一套属于自己的 Neovim 配置,主题、快捷键、插件配置等等都比较顺手了,也逐渐习惯用 Neovim 编辑一切,事实上,我在此网站发布的文章,包括目前正在写的这篇,都是我用 Neovim 以 Markdown 格式编辑完成的。但是随着时间的流逝,我也遇到一些问题,主要的问题是我的配置大多都是东拼西凑从互联网各处抄来的,配置文件写得比较杂乱且没有很好地作注释,时间一长搞得我自己也看不懂我的配置文件了,遇到一些问题要排查就要废挺多脑筋。

网上也存在一些现成的 Neovim 配置,或者叫「Neovim 发行版」,比如 LazyVimLunarVimNvChad 等等,这些所谓的发行版都提供了开即用的使用体验,不过我个人倒是觉得这些发行版有些「重」了,根据我自己的使用情况,可能根本用不到里面的很多高级功能,而且后期自定义与添加插件也多少有点不方便。

就在最近,我了解到了一个项目 kickstart.nvim,按照项目的介绍,这并不是一个 Neovim 发行版,而是一套 Neovim 配置的「出发点」,追求用最少的配置达到可以开箱即用的功能,同时对配置内容做了很详尽的注释,用户可以在这一套配置之上进一步自定义,打造属于自己的 Neovim 配置。

在详细看了一遍项目的内容后,发现我常用的绝大多数功能,在这个项目中都已经配置好了,而且没有太多多余的功能。所以我打算,在这个项目的基础上,重新自定义一套我自己的配置,并对配置内容做好整理与注释,方便未来的自己进一步管理。

开始安装

前置要求

首先,要学会如何退出 Vim,也就是说了解一些 Vim 的基础操作,比如一些基础的移动、编辑、保存与退出之类的。

之后,要了解一些 Lua 的基本用法,因为这个项目的配置文件是用 Lua 编写的,不了解也没关系,我就是在自己折腾过程中慢慢了解 Lua 的语法的。

接着,需要安装最新稳定版或 nightly 版本的 Neovim,旧版本的 Neovim 可能有些特性不兼容,原版的 Vim 也不行,因为不支持 lua 配置文件。

最后,安装项目推荐的 外部依赖,如 git、make、unzip、gcc 等等,以及可选安装 Nerd Font 字体用来在终端显示图标。

安装

如果你已经有一套 Neovim 配置了,需要将位于 ~/.config/nvim/ 的配置文件备份到别处,并将配置文件目录 ~/.config/nvim/ 与数据目录 ~/.local/share/nvim/ 删除。

或者也可以让新旧配置文件共存,添加环境变量 NVIM_APPNAME 并运行 nvim,比如 NVIM_APPNAME="nvim-kickstart" nvim 就可以让 Neovim 以 ~/.config/nvim-kickstart 作为配置文件目录启动,数据目录也会相应的变为 ~/.local/share/nvim-kickstart/,我个人是在 .zshrc 中添加了一行 alias:

alias k='NVIM_APPNAME="nvim-kickstart" nvim'

这样就可以用命令 k 来启动新的 Neovim 配置文件,而常规的 nvim 命令则是启动旧的配置文件。

之后将 kickstart 项目克隆到本地,我个人并没有用原版的 kickstart,而是用的它的一个 Fork,kickstart-modular,相比于原版的 kickstart,这个项目将配置文件拆成了多份,在管理上更方便一些,实际内容是一样的。

git clone https://github.com/dam9000/kickstart-modular.nvim.git "${XDG_CONFIG_HOME:-$HOME/.config}"/nvim

当然如果选择了新旧配置文件共存,需要相应修改路径:

git clone https://github.com/dam9000/kickstart-modular.nvim.git "${XDG_CONFIG_HOME:-$HOME/.config}"/nvim-kickstart

另外文档中也比较推荐自己将仓库 Fork 下来,并克隆自己的 Fork 仓库,这样在后期比较方便版本管理与维护。

之后重新打开 Neovim,插件管理器 Lazy 会自动下载安装插件,之后呈现在你面前的就是一个完全配置好开箱即用的 Neovim 配置了。

之后你就可以探索这套配置可以用来做什么了。它用了 Which Key 插件用来管理快捷键,只要按下 leader 键(在这套配置里是空格键),并停顿半秒钟,屏幕上面就会显现目前所有可用的快捷键了;


也可以仔细阅读配置文件中的注释内容,并了解其中每个插件的作用;或者也可以观看作者录制的介绍视频,以对这个项目有更直观的了解。

自定义配置

在了解了这个项目的大致内容后,就需要根据自己的需要对配置进行自定义修改了,在这一节内容中我会 kickstart 的配置进行修改,以满足我自己的使用习惯。

内置选项

kickstart 在配置文件中有些选项是默认禁用的没有启用,我们可以根据自己的喜好将其启用,也可以禁用其中一些选项。

首先编辑 init.lua 文件,找到 vim.g.have_nerd_font = false,这个选项默认禁用了 Nerd Font 图标字体,使用 Unicode emoji 作为图标字体,如果你的终端已经配置好了 Nerd Font 的话,可以将这个选项改为 true,这样界面中的图标就会以 Nerd Font 图标字体显示了。

之后编辑 lua/options.lua 文件。首先找到 vim.opt.relativenumber = true,这个选项控制显示相对于当前光标所在行的相对行号,打开这个选项后就可以比较方便地跳转多行。这一行是被注释掉的,删掉前面的 -- 就可以取消注释,另外因为安装了 Comment.nvim 插件,可以直接在 Visual 模式中选中这一行,按快捷键 gc 来快速取消注释。

接着找到 vim.opt.clipboard = 'unnamedplus',这个选项可以让 Neovim 与系统共享剪贴板,不过我个人倒不太喜欢这个功能,因为 Vim 每一次剪切或删除操作都会把内容保存进剪贴板,如果与系统共享剪贴板,会导致系统剪贴板被一大堆无意义的内容占据,之后我会定义一个快捷键,专门用来将内容复制到系统剪贴板。同样选中这一行按 gc 就可以将内容注释掉。

然后找到 vim.opt.undofile = true,这个选项会将文件的修改历史保存到磁盘里,这样即使退出重新打开文件也可以找到之前的修改历史,不过我个人不太喜欢这个功能,所以将其注释掉了,如果觉得这个功能有用也可以保留。

快捷键

我个人主要添加了两组快捷键,一个是快速切换与关闭 buffer,另一个是将当前选中内容复制进系统剪贴板,编辑 lua/keymaps.lua,在最后插入下面几行:

-- Changeing buffer
vim.keymap.set('n', '<leader>l', ':bnext<CR>', { desc = 'Switch to Next buffer' })
vim.keymap.set('n', '<leader>h', ':bprevious<CR>', { desc = 'Switch to Previous buffer' })
vim.keymap.set('n', '<leader>bd', ':bd<CR>', { desc = 'Delete Current Buffer' })
vim.keymap.set('n', '<leader>b!', ':bd!<CR>', { desc = 'Force Delete Current Buffer' })

-- Yanking into clipboard
vim.keymap.set('v', '<leader>y', [["+y]], { desc = 'Yanking into Clipboard' })

kickstart 配置文件中已经定义了 leader 键是空格键,所以上面的快捷键就是:在打开了多个 buffer 的情况下,空格+l空格+h 用来在 Buffer 间快速切换,空格+bd 快速关闭当前 buffer,空格+b! 强制关闭当前 buffer 不询问是否保存。还有最后一个 空格+y 将当前选中内容复制进系统剪贴板。

不过 空格+h 的快捷键与 gitsigns 插件定义的快捷键冲突了,所以还需要改一下 gitsigns 的快捷键,编辑 lua/kickstart/plugins/gitsigns.lua 文件,找到下面的内容:

        -- visual mode
        map('v', '<leader>hs', function()
          gitsigns.stage_hunk { vim.fn.line '.', vim.fn.line 'v' }
        end, { desc = 'stage git hunk' })
        map('v', '<leader>hr', function()
          gitsigns.reset_hunk { vim.fn.line '.', vim.fn.line 'v' }
        end, { desc = 'reset git hunk' })
        -- normal mode
        map('n', '<leader>hs', gitsigns.stage_hunk, { desc = 'git [s]tage hunk' })
        map('n', '<leader>hr', gitsigns.reset_hunk, { desc = 'git [r]eset hunk' })
        map('n', '<leader>hS', gitsigns.stage_buffer, { desc = 'git [S]tage buffer' })
        map('n', '<leader>hu', gitsigns.undo_stage_hunk, { desc = 'git [u]ndo stage hunk' })
        map('n', '<leader>hR', gitsigns.reset_buffer, { desc = 'git [R]eset buffer' })
        map('n', '<leader>hp', gitsigns.preview_hunk, { desc = 'git [p]review hunk' })
        map('n', '<leader>hb', gitsigns.blame_line, { desc = 'git [b]lame line' })
        map('n', '<leader>hd', gitsigns.diffthis, { desc = 'git [d]iff against index' })
        map('n', '<leader>hD', function()
          gitsigns.diffthis '@'
        end, { desc = 'git [D]iff against last commit' })

改成:

        -- visual mode
        map('v', '<leader>gs', function()
          gitsigns.stage_hunk { vim.fn.line '.', vim.fn.line 'v' }
        end, { desc = 'stage git hunk' })
        map('v', '<leader>gr', function()
          gitsigns.reset_hunk { vim.fn.line '.', vim.fn.line 'v' }
        end, { desc = 'reset git hunk' })
        -- normal mode
        map('n', '<leader>gs', gitsigns.stage_hunk, { desc = 'git [s]tage hunk' })
        map('n', '<leader>gr', gitsigns.reset_hunk, { desc = 'git [r]eset hunk' })
        map('n', '<leader>gS', gitsigns.stage_buffer, { desc = 'git [S]tage buffer' })
        map('n', '<leader>gu', gitsigns.undo_stage_hunk, { desc = 'git [u]ndo stage hunk' })
        map('n', '<leader>gR', gitsigns.reset_buffer, { desc = 'git [R]eset buffer' })
        map('n', '<leader>gp', gitsigns.preview_hunk, { desc = 'git [p]review hunk' })
        map('n', '<leader>gb', gitsigns.blame_line, { desc = 'git [b]lame line' })
        map('n', '<leader>gd', gitsigns.diffthis, { desc = 'git [d]iff against index' })
        map('n', '<leader>gD', function()
          gitsigns.diffthis '@'
        end, { desc = 'git [D]iff against last commit' })

基本就是把 h 开头的快捷键改成了 g 开头。

之后还需要改一下 which-key 的快捷键提示,编辑 lua/kickstart/plugins/which-key.lua,找到如下内容:

      require('which-key').register {
        ['<leader>c'] = { name = '[C]ode', _ = 'which_key_ignore' },
        ['<leader>d'] = { name = '[D]ocument', _ = 'which_key_ignore' },
        ['<leader>r'] = { name = '[R]ename', _ = 'which_key_ignore' },
        ['<leader>s'] = { name = '[S]earch', _ = 'which_key_ignore' },
        ['<leader>w'] = { name = '[W]orkspace', _ = 'which_key_ignore' },
        ['<leader>t'] = { name = '[T]oggle', _ = 'which_key_ignore' },
        ['<leader>h'] = { name = 'Git [H]unk', _ = 'which_key_ignore' },
      }
      -- visual mode
      require('which-key').register({
        ['<leader>h'] = { 'Git [H]unk' },
      }, { mode = 'v' })

改成:

      require('which-key').register {
        ['<leader>c'] = { name = '[C]ode', _ = 'which_key_ignore' },
        ['<leader>d'] = { name = '[D]ocument', _ = 'which_key_ignore' },
        ['<leader>r'] = { name = '[R]ename', _ = 'which_key_ignore' },
        ['<leader>s'] = { name = '[S]earch', _ = 'which_key_ignore' },
        ['<leader>w'] = { name = '[W]orkspace', _ = 'which_key_ignore' },
        ['<leader>t'] = { name = '[T]oggle', _ = 'which_key_ignore' },
        ['<leader>g'] = { name = '{G]it Hunk', _ = 'which_key_ignore' },
        ['<leader>b'] = { name = '[B]uffer', _ = 'which_key_ignore' },
      }
      -- visual mode
      require('which-key').register({
        ['<leader>g'] = { '[G]it Hunk' },
      }, { mode = 'v' })

启用内置插件

kickstart 还有一些内置的插件默认没有启用,我想要启用 autopairs 和 neo-tree 这两个插件,autopairs 可以自动补全括号、双引号这些成对的符号,而 neo-tree 则可以在侧边栏显示一个文件树用来管理文件,按下反斜杠 \ 就可以显示或隐藏文件树。


要启用这两个插件也很简单,编辑 lua/lazy-plugins.lua,找到以下两行并取消注释:

  -- require 'kickstart.plugins.autopairs',
  -- require 'kickstart.plugins.neo-tree',

另外我还想要禁用一个插件 mini.nvim,这是个很多功能简单的小插件的集合,目前下面的状态栏就是用它实现的,不过我后面打算用其他插件来代替它,所以先将它禁用了,找到 require 'kickstart/plugins/mini', 并将其注释掉。

之后重新打开 Neovim,就会自动开始下载缺失的插件,之后输入 :Lazy 回车打开 Lazy 的主界面,会看到 mini.nvim 已经被禁用了,按大写 X 就可以将禁用的插件卸载。

此外,kickstart 还有其他插件默认没有启用,比如 nvim-dap 用来 debug,indent-blankline 用来显示缩进,nvim-lint 用来语法检查。

自动补全

kickstart 使用 nvim-cmp 插件进行自动补全,这个插件的配置项十分丰富,kickstart 默认只配置了 lsp 与文件系统路径作为补全来源,虽然配置了 Luasnip 来补全代码片段,但没有启用代码片段的来源,而且 kickstart 对 nvim-cmp 的快捷键设定我也不太习惯。所以对这个插件的配置修改会比较复杂。

编辑 lua/kickstart/plugins/cmp.lua,首先添加更多补全来源,找到下面的内容并取消注释,这启用了 friendly-snippets 插件,包含了很多常用的适用于各种文件格式的代码补全片段:

          -- {
          --   'rafamadriz/friendly-snippets',
          --   config = function()
          --     require('luasnip.loaders.from_vscode').lazy_load()
          --   end,
          -- },

另外我还添加了一些我自己写的代码片段,位于 ~/.config/nvim/snippets,主要是在 Markdown 文档里方便地输入直角引号,具体的内容可以看这里。所以修改完之后应该是这样的:

          {
            'rafamadriz/friendly-snippets',
            config = function()
              require('luasnip.loaders.from_vscode').lazy_load()
              require('luasnip.loaders.from_vscode').lazy_load { paths = { '~/.config/nvim/snippets' } }
            end,
          },

之后再在插件列表中添加两个插件分别用来补全来自 buffer 中的内容和 vim 命令行的内容:

      'hrsh7th/cmp-buffer',
      'hrsh7th/cmp-cmdline',

再在文件最后添加刚刚的补全源:

        sources = {
          { name = 'nvim_lsp' },
          { name = 'luasnip' },
          { name = 'path' },
          { name = 'buffer' },
        },
        -- 前面三个是之前就有的,最后的 buffer 是要添加的

如果你想在 vim 命令行中或使用 / 搜索内容时显示补全,则需要添加以下几行:

      -- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore).
      cmp.setup.cmdline({ '/', '?' }, {
        mapping = cmp.mapping.preset.cmdline(),
        sources = {
          { name = 'buffer' },
        },
      })

      -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
      cmp.setup.cmdline(':', {
        mapping = cmp.mapping.preset.cmdline(),
        sources = cmp.config.sources({
          { name = 'path' },
        }, {
          { name = 'cmdline' },
        }),
        matching = { disallow_symbol_nonprefix_matching = false },
      })

默认 nvim-cmp 只会在进入 Insert 模式后才会启用,要想让它在 vim 命令行下也可以自动启用,需要添加相关的 event,找到 event = 'InsertEnter', 这一行,改成 event = { 'InsertEnter', 'CmdlineEnter' },,这样 nvim-cmp 就会在进入 vim 命令行模式时自动启用了。

接着是自定义快捷键,kickstart 使用 ctrl+nctrl+p 来上下选择不同的补全项,用 ctrl+y 来选中补全项,对于代码片段,使用 ctrl+lctrl+h 来跳转上一个和下一个占位符。如果你可以接受这个快捷键设定,则无需折腾了,不过我更习惯使用 TabShift+Tab 来选择补全项,回车来选中补全项。好在这种快捷键在 nvim-cmp 的文档里面有教程。首先禁用先前定义的快捷键,找到下面这些行,将其注释掉:

          ['<C-n>'] = cmp.mapping.select_next_item(),

          ['<C-p>'] = cmp.mapping.select_prev_item(),

          ['<C-y>'] = cmp.mapping.confirm { select = true },

          ['<C-l>'] = cmp.mapping(function()
            if luasnip.expand_or_locally_jumpable() then
              luasnip.expand_or_jump()
            end
          end, { 'i', 's' }),
          ['<C-h>'] = cmp.mapping(function()
            if luasnip.locally_jumpable(-1) then
              luasnip.jump(-1)
            end
          end, { 'i', 's' }),
          -- 这些行在配置文件中应该是分散开的

然后添加新的快捷键:

          ['<CR>'] = cmp.mapping(function(fallback)
            if cmp.visible() then
              if luasnip.expandable() then
                luasnip.expand()
              else
                cmp.confirm {
                  select = true,
                }
              end
            else
              fallback()
            end
          end),

          ['<Tab>'] = cmp.mapping(function(fallback)
            if cmp.visible() then
              cmp.select_next_item()
            elseif luasnip.locally_jumpable(1) then
              luasnip.jump(1)
            else
              fallback()
            end
          end, { 'i', 's' }),

          ['<S-Tab>'] = cmp.mapping(function(fallback)
            if cmp.visible() then
              cmp.select_prev_item()
            elseif luasnip.locally_jumpable(-1) then
              luasnip.jump(-1)
            else
              fallback()
            end
          end, { 'i', 's' }),

再之后还可以自定义补全菜单的外观,比如说我想在补全菜单中显示图标,首先在插件列表中添加 onsails/lspkind.nvim 这个插件,之后在 config = function() 这个函数里面添加一行 local lspkind = require('lspkind'),并在 cmp.setup {} 这个 lua table 里面添加如下内容:

        formatting = {
          format = lspkind.cmp_format {
            mode = 'symbol_text',
            menu = {
              buffer = '[Buffer]',
              nvim_lsp = '[LSP]',
              path = '[Path]',
              luasnip = '[Snippet]',
              cmdline = '[Cmdline]',
            },
          },
        },

其中的 mode 可以选择 texttext_symbolsymbol_text 以及 symbol,分别对应只显示文字、先文字后图标、先图标后文字以及只显示图标,menu 里面是自定义各个补全来源显示的名字。

另外我还配置了显示 Ghost Text,即在光标后面显示预补全内容:

        experimental = {
          ghost_text = true,
        },

如果觉得这部分的配置太过复杂,这里是我完整的 nvim-cmp 的配置文件,直接抄作业就行了。

通过 nvim-cmp,我可以在编辑 Markdown 文档时快速地输入一些内容,比如各级标题、图片、链接、表格等等

LSP 配置

LSP 的全称是「Language Server Protocol」,是微软开发并开源的一个协议,简单来说就是可以让 Language Server 与编辑器相互沟通,从而为代码提供补全、诊断、Code Action 等等接近完整 IDE 的功能。Neovim 原生支持 LSP,kickstart 使用 nvim-lspconfig 插件来配置 Language Server,用 Mason.nvim 插件来自动安装 Language Server。

kickstart 默认为 lua 文件配置了 Lua_ls 和 stylua 作为 Language Server。在上一节配置完 nvim-cmp 后,你会发现在配置文件中出现了一些 Missing fileds 警告:


但实际上这个警告是不影响功能的,如果觉得这个警告比较恼人,可以选择禁用它,编辑 lua/kickstart/plugins/lspconfig.lua,找到下面一行取消注释即可:

              -- diagnostics = { disable = { 'missing-fields' } },

如果为其他语言配置 Language Server,只需运行 :Mason 回车打开 Mason 插件的界面,选择自己想要安装的 Language Server 安装即可,或者也可以在配置文件中指定自动安装:

      vim.list_extend(ensure_installed, {
        'stylua', -- Used to format Lua code
        'marksman', -- Used for markdown file
      })

另外也可以自定义代码诊断的图标 ,在 config = function() 后面添加下面这几行:

      vim.diagnostic.config {
        virtual_text = false,
        signs = true,
        underline = true,
        update_in_insert = false,
        severity_sort = false,
      }

      -- Diagnostics icons
      local signs = { Error = '󰅚 ', Warn = '󰀪 ', Hint = '󰌶 ', Info = ' ' }
      for type, icon in pairs(signs) do
        local hl = 'DiagnosticSign' .. type
        vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
      end

更多 LSP 相关的自定义可以查看 nvim-lspconfig 的 Wiki

因为我并不是程序员,LSP 对我来说用处并不大,所以我只打算在一些特定的文件中启用它,在配置文件中添加一行 ft = { 'markdown', 'lua' },,会让插件只在 Markdown 文件和 Lua 文件中启用,在其他情况下禁用,可以提升一点启动速度。在 Lua 中启用可以让我更好地编辑 Neovim 配置文件,而在 Markdown 中则可以用 marksman 提供的 Code Action 来方便地添加标题目录。

文档格式化

在编辑代码或是文档时,虽然一些格式上的不标准可能并不会影响最终的效果,但对于其他人来说可读性就要差很多,但是大部分时候我们可能没有精力去关心格式是否标准,或者出现了格式错误也很难及时发现,这时就需要借助一些工具来格式化文档了。

kickstart 使用 conform.nvim 插件来格式化文档,并通过 Mason 插件来自动安装格式化工具,conform 支持的工具列表可以看这里 。默认已经配置了使用 stylua 来为 lua 文件格式化,在编辑完 lua 文件后,保存时便可自动格式化,也可以使用快捷键 空格+f 来手动格式化。

如果不太喜欢在每次保存时自动格式化,可以选择禁用这个功能,编辑 lua/kickstart/plugins/conform.lua 文件,找到下面的内容注释掉:

      format_on_save = function(bufnr)
        -- Disable "format_on_save lsp_fallback" for languages that don't
        -- have a well standardized coding style. You can add additional
        -- languages here or re-enable it for the disabled ones.
        local disable_filetypes = { c = true, cpp = true }
        return {
          timeout_ms = 500,
          lsp_fallback = not disable_filetypes[vim.bo[bufnr].filetype],
        }
      end,

并替换为:

      format_on_save = false,

我另外还为 Markdown 文档配置了使用 autocorrect 来进行格式化,可以实现在中英文混输的文档里面为中英文之间自动添加空格,更正标点符号等操作:

      formatters_by_ft = {
        lua = { 'stylua' },
        markdown = { 'autocorrect' },
        -- Conform can also run multiple formatters sequentially
        -- python = { "isort", "black" },
        --
        -- You can use a sub-list to tell conform to run *until* a formatter
        -- is found.
        -- javascript = { { "prettierd", "prettier" } },
      },

不过 autocorrect 目前不支持使用 Mason 来自动安装,需要手动将其安装到当前系统的 $PATH 里面,具体可以参考项目 README 的安装部分。另外我还发现使用 autocorrect 最新的 2.11.1 版本,在每次格式化后文件的最后会多出一个空行,而上一个版本 2.10.0 就没这个问题,如果介意的话,可以选择安装旧版本。

为了更快地启动 Neovim,我还配置了让 conform 只在 Lua 和 Markdown 文件中启用,找到 lazy = false, 一行,将其注释掉,并在其后面添加一行:ft = { 'lua', 'markdown' },

懒加载更多插件

kickstart 使用 Lazy.nvim 来管理插件,从其名字也可以推测出来,这个插件管理器一大特色功能就是懒加载,Lazy 对插件懒加载的配置可谓十分灵活,可以配置让插件在不同的情况下懒加载。让插件只在必须的时候进行加载,可以很大程度上提升 Neovim 的启动速度。

kickstart 默认已经为一些插件配置了懒加载,还有些插件我前文的配置过程中也按照我自己的使用情况配置了懒加载,不过还有一些插件默认没有配置懒加载,虽然其中大部分插件对启动速度影响并不大,但积少成多还是能省出一点启动时间的。

首先是 vim-sleuth 和 Comment.nvim,这两个插件大概是作为演示作用,没有配置懒加载,也没有放进单独的配置文件,而是直接写在 lua/lazy-plugins.lua 里面。

vim-sleuth 插件可以自动检测并调整当前文本的 tab 缩进,所以实际上只有在加载 buffer 或是 新建 buffer 的时候才真正有用,所以找到 'tpope/vim-sleuth', 这一行,改成:

  {
    'tpope/vim-sleuth', -- Detect tabstop and shiftwidth automatically
    event = { 'BufReadPre', 'BufNewFile' },
  },

Comment.nvim 插件可以快捷地对文档进行注释,这个功能在前文已经介绍过了,我自己实际上也只用到了它的 gc 快捷键,所以我打算让它只在按下这个快捷键的时候再启用,找到 { 'numToStr/Comment.nvim', opts = {} }, 一行,改成:

  {
    'numToStr/Comment.nvim',
    keys = { 'gc' },
    opts = {},
  },

todo-comments.nvim 这个插件可以自动高亮代码中包含 TODONOTE 等关键词的注释段落,让注释更有可读性,这个插件也只有在加载 buffer 的时候才有用,编辑 lua/kickstart/plugins/todo-comments.lua,找到 event = 'VimEnter', 改成 event = { 'BufRead', 'BufNewFile' },

gitsigns.nvim 插件提供了 git 的集成,也只有在加载 buffer 的时候才有用,编辑 lua/kickstart/plugins/gitsigns.lua 添加一行:event = { 'BufReadPre', 'BufNewFile' },

which-key.nvim 插件可以帮助显示快捷键,所以在不使用快捷键的时候自然也用不到,编辑 lua/kickstart/plugins/which-key.lua,将 event = 'VimEnter', 改成 event = 'VeryLazy',。这个的意思大概是在其他东西都加载完之后再加载它,我也是从其他人的配置里抄来的。

nvim-treesitter 插件主要用来显示代码高亮,我为它配置了只在加载 buffer 的时候启用,同样加一行 event = { 'BufReadPre', 'BufNewFile' }, 即可。

telescope.nvim 是一个类似于 Fzf 的模糊查找器,但是功能更强大,telescope 与 kickstart 是同一个作者写的(同时这个作者也是 Neovim 项目的主要维护者之一),自然 kickstart 中有很多功能都用到了 telescope,打开 Lazy 的主界面也可以看到 telescope 的启动占用了挺长的时间。为了让 telescope 实现懒加载而不影响到现有的功能,需要将 telescope 的快捷键重新注册一遍,编辑 lua/kickstart/plugins/telescope.lua,找到 event = 'VimEnter', 并将其注释掉,并在后面添加如下内容:

    keys = {
      { '<leader>sh', desc = '[S]earch [H]elp' },
      { '<leader>sk', desc = '[S]earch [K]eymaps' },
      { '<leader>sf', desc = '[S]earch [F]iles' },
      { '<leader>ss', desc = '[S]earch [S]elect Telescope' },
      { '<leader>sw', desc = '[S]earch current [W]ord' },
      { '<leader>sg', desc = '[S]earch by [G]rep' },
      { '<leader>sd', desc = '[S]earch [D]iagnostics' },
      { '<leader>sr', desc = '[S]earch [R]esume' },
      { '<leader>s.', desc = '[S]earch Recent Files ("." for repeat)' },
      { '<leader><leader>', desc = '[ ] Find existing buffers' },
      { '<leader>/', desc = '[/] Fuzzily search in current buffer' },
      { '<leader>s/', desc = '[S]earch [/] in Open Files' },
      { '<leader>sn', desc = '[S]earch [N]eovim files' },
    },

意思是 telescope 只会在按下上面这些快捷键的时候才会启用,并添加了功能描述方便 which-key 识别。

经过上面这些配置,我这个性能相当弱鸡的笔记本,Neovim 的启动速度也可以控制在 50ms 以下了。

添加更多插件

为了满足我的使用习惯,我还需要添加更多插件,好在 kickstart 安装额外插件也比较简单,只要编辑 lua/lazy-plugins.lua 文件,找到 -- { import = 'custom.plugins' }, 一行取消注释,之后将想要添加的插件配置放到 lua/custom/plugins 文件夹里,重新启动 Neovim,就可以自动安装了。

主题配色

使用什么样的主题配色这个完全是看个人喜好了,kickstart 默认使用了 tokyonight 主题配色,不过我个人更喜欢 Gruvbox 配色,要使用新的主题配色,首先禁用原来的的 tokyonight 主题,编辑 lua/lazy-plugins.lua 文件,找到 require 'kickstart/plugins/tokyonight', 并注释掉。然后添加新的主题插件,新建 lua/custom/plugins/gruvbox.lua 文件,然后重启 Neovim 即可:

return {
  {
    'ellisonleao/gruvbox.nvim',
    priority = 1000, -- Make sure to load this before all the other start plugins.
    init = function()
      vim.o.background = 'dark' -- or "light" for light mode
      vim.cmd [[colorscheme gruvbox]]
    end,
  },
}

但是这样你会发现虽然大部分时候 Neovim 都是用的 Gruvbox 主题,在 Lazy 在安装新插件的时候还是用的之前的主题配色,这是因为 Lazy 默认在安装插件时使用了不同的主题配色,这可能是为了在主题插件还没准备好时防止 Neovim 的默认配色闪瞎眼。你也可以配置 Lazy 在安装插件时使用的主题配色,编辑 lua/lazy-plugins.lua 文件,在最后一个括号前插入一行:

  install = { colorscheme = { 'gruvbox' } },

当然如果 gruvbox 插件没装的话这个设置是不起作用的。

状态栏

在前面的内容里面我禁用了 mini.nvim 插件,其中就包括状态栏插件,我要用另一个状态栏插件来代替它,就是 lualine.nvim。新建 lua/custom/plugins/lualine.lua

return {
  'nvim-lualine/lualine.nvim',
  dependencies = { 'nvim-tree/nvim-web-devicons' },
  event = "VeryLazy",
  config = function()
    require('lualine').setup {
      options = {
        theme = 'gruvbox',
      },
      sections = {
        lualine_a = { 'mode' },
        lualine_b = { 'branch', 'diff', 'diagnostics' },
        lualine_c = { 'filename', 'lsp_progress' },
        lualine_x = {
          {
            require('lazy.status').updates,
            cond = require('lazy.status').has_updates,
            --color = { fg = "#a89984" },
          },
          'encoding',
          'fileformat',
          'filetype',
        },
        lualine_y = { 'progress' },
        lualine_z = { 'location' },
      },
      tabline = {
        lualine_a = { 'tabs' },
        lualine_b = { 'buffers' },
        lualine_c = {},
        lualine_x = {},
        lualine_y = {},
        lualine_z = {},
      },
    }
  end,
}

除了常规的底部状态栏,我还在顶部添加了标签栏和 buffer 栏,方便结合前面设定的快捷键快速跳转 buffer。

另外我还添加了一个功能用来在状态栏上显示当前有多少插件可以更新,我也忘了是在哪里抄来的了,不过要想这个功能可用,还需要为 Lazy 启用自动检查更新,编辑 lua/lazy-plugins.lua,在最后一个括号前插入一行:

  checker = { enabled = true, frequency = 600, notify = false },

这个的意思是启用自动检查更新,10 分钟检查一次,并禁用更新通知(因为有更新的话会在状态栏上显示)。

更多的配置项可以查看 lualine 的文档

输入法切换

使用 Vim 编辑中文时,最恼人的一点就是输入法问题,在 Insert 模式输入完中文并退出到 normal 模式后,必须将输入法手动切换到英文模式才可以正常使用,不过还好这个问题也有很多插件可以解决,我目前正在用的插件是 vim-barbaric,可以支持在退出 Insert 模式时自动切换到英文输入法,进入 Insert 模式又切换回中文,我用的 Fcitx5 输入法,可以完美支持,其他输入法请自行测试。新建 lua/custom/plugins/barbaric.lua

return {
  'rlue/vim-barbaric',
  event = 'InsertEnter',
}

我配置了懒加载,让它在进入 Insert 模式时再启用。

快速跳转

使用 Vim 编辑中文另一个让人头疼的问题是几乎完全不支持中文分词,Vim 快捷键用来跳转上一个与下一个单词的快捷键 wb,在中文文档中会直接跳转一整句,要想在中文中实现指哪打哪的快速跳转,就需要借助 vim-easymotionvim-easymotion-zh 这两个插件了。vim-easymotion 实现了在 Vim 中搜索并快速跳转,而 vim-easymotion-zh 则在其基础上实现了使用双拼搜索中文。新建 lua/custom/plugins/easymotion.lua

return {
  'zzhirong/vim-easymotion-zh',
  dependencies = { 'easymotion/vim-easymotion' },
  config = function()
    vim.cmd [[
    let g:EasyMotion_leader_key=";"
    let g:EasyMotion_skipfoldedline=0
    let g:EasyMotion_space_jump_first=1
    let g:EasyMotion_move_highlight = 0
    let g:EasyMotion_use_migemo = 1
    ]]
  end,
  keys = {
    {
      '<leader>sp',
      '<Plug>(easymotion-overwin-f2)',
      mode = 'n',
      desc = 'Search Current Page',
    },
  },
}

我定义了一个快捷键 空格+sp,按下快捷键后,输入要跳转内容的前两个英文字母,或是中文的小鹤双拼码,比如「键」这个字就是「jm」,输入后会显示当前页面所有符合搜索结果的位置,按下对应的按键就可快速跳转。当然这个功能只有比较熟悉小鹤双拼的人才能用的比较顺手吧,惯用其他双拼方案的人可以在原作者基础上进行修改,如果根本不熟悉双拼,则可以看看 vim-easymotion-chs

记住光标位置

Neovim 默认不会记住上次关闭文件时光标所在的位置,所以每次打开文件时光标都会在第一行,这有时挺烦人的,为了让 Neovim 记住上一次光标的位置,可以添加 vim-lastplace 这个插件,新建 lua/custom/plugins/lastplace.lua

return {
  'farmergreg/vim-lastplace',
  event = { 'BufReadPre', 'BufNewFile' },
}

处理大文件

在打开一些比较大的文档时,一些插件,比如 lspconfig 和 treesitter,可能会拖慢 Neovim 的加载速度,严重时会导致 Neovim 卡死,bigfile.nvim 这个插件会在打开大文件时自动禁用一些会拖慢 Neovim 的功能,从而加快 Neovim 的加载,新建 lua/custom/plugins/bigfile.lua

return {
  'LunarVim/bigfile.nvim',
  event = { "FileReadPre", "BufReadPre", "User FileOpened" },
}

这个配置是我从 LunarVim 项目里抄来的,触发文件的大小与禁用哪些功能都可以单独配置,具体可以看项目的文档。

Markdown 预览

markdown-preview.nvim 插件可以在编辑 Markdown 文档时打开一个浏览器页面实时显示文档渲染效果,实现所见即所得。新建 lua/custom/plugins/markdown-preview.lua

return {
  'iamcco/markdown-preview.nvim',
  build = function()
    vim.fn['mkdp#util#install']()
  end,
  ft = 'markdown',
  config = function()
    vim.g.mkdp_filetypes = { 'markdown' }
    vim.cmd [[let g:mkdp_browser = 'firefox']]
  end,
  keys = {
    {
      '<leader>m',
      ':MarkdownPreviewToggle<CR>',
      mode = '',
      desc = '[M]arkdown Preview',
    },
  },
}


这里我配置了使用 firefox 来预览 Markdown 文档,定义了一个快捷键 空格+m 来快速打开预览。不过需要注意的是,插件第一次安装好后还需要下载额外文件,如果下载失败,会导致功能不可用。

包裹符号

vim-surround 插件可以方便地删除、添加以及修改包裹符号,包括括号、引号等等,比如我常用的一个功能,选中一个词,按大写 S,再按下想要将其包裹的符号,比如引号,就可以快速将其包裹上引号。添加 lua/custom/plugins/surround.lua

return {
  'tpope/vim-surround',
  event = { 'CursorMoved', 'CursorMovedI' },
}

更多这个插件的高级用法,可以查看项目的文档。

更好的修改历史预览

在 Vim 中在 Normal 模式下按下 u 就可以撤销之前的修改,而若是想要更直观地查看自己的修改历史并快速撤销修改,就需要 undotree 这个插件了,这个插件在侧边显示一个菜单用来显示之前的修改历史,并可以在不同的修改历史之间快速跳转。添加 lua/custom/plugins/undotree.lua

return {
  'mbbill/undotree',
  keys = {
    {
      '<leader>u',
      ':UndotreeToggle<CR>',
      mode = 'n',
      desc = 'Toggle Undo Tree',
    },
  },
}

我定义了一个快捷键 空格+u 用来显示或隐藏侧边菜单。

打开终端

在编辑文本时,有时需要临时打开终端执行一些命令,这时就需要退出编辑器,执行完命令后再重新打开编辑器,这就有些麻烦了,而 toggleterm.nvim 这个插件就解决了这个问题,这个插件允许在 Neovim 中临时打开一个终端。新建 lua/custom/plugins/toggleterm.lua

return {
  'akinsho/toggleterm.nvim',
  config = function()
    require('toggleterm').setup {
      size = 20,
      open_mapping = [[<c-\>]],
      direction = 'float',
      float_opts = {
        border = 'curved',
      },
    }

    function _G.set_terminal_keymaps()
      local opts = { buffer = 0 }
      vim.keymap.set('t', 'jk', [[<C-\><C-n>]], opts)
    end

    -- if you only want these mappings for toggle term use term://*toggleterm#* instead
    vim.cmd 'autocmd! TermOpen term://* lua set_terminal_keymaps()'
  end,
  keys = [[<c-\>]],
  cmd = 'ToggleTerm',
}

这个插件默认使用 ctrl+\ 来打开终端,或者也可以用命令 ToggleTerm 来打开终端,使用快捷键 jk 来进入「vim 模式」,从而用 vim 快捷键复制当前终端中的任意内容,快捷键 i 回到终端模式。而且在打开的终端中,还可以再套娃打开一个 Neovim,只要屏幕够大,就可以无限套娃下去,虽然这样并没有什么用就是了。

结尾

我目前已经将我的 Neovim 配置完全迁移到了基于 kickstart 的配置,相比于旧的配置文件,新的配置不但有更丰富的功能,还有更清晰的结构,方便我后期维护。

这里是我完整的配置文件,想要抄作业的朋友可以直接来看,不过还是推荐在 kickstart 的基础上进行自定义,这样更符合自己的使用习惯,而且能对 Neovim 配置文件的结构有更好的理解。