跳过正文
  1. Posts/

Vim 学习笔记 Day 012:LazyVim 代码导航与 LSP 基础操作,把“会搜文本”推进到“会按语义跳转”

·4992 字·10 分钟
📖 阅读 --
DogDu
作者
DogDu
工作结束或者累了, 要不休息一会, 看会动漫吧 ~
目录
Vim 14 天 - 这篇文章属于一个选集。
§ 12: 本文

今日主题
#

  • 主主题:LazyVim 代码导航与 LSP 基础操作
  • 副主题:把 Day 011 的“找文件 / 搜内容 / 切上下文”,接到“按语义跳定义、查引用、看符号、做重命名”这条链路上

学习目标
#

  • 建立一个关键判断:
    • grep 是文本搜索
    • LSP 是语义导航
  • 把 Day 12 最常用的一组默认入口练成第一反应:
    • gd
    • gr
    • gI
    • gy
    • gD
    • K
    • <leader>ca
    • <leader>cr
    • <leader>ss
  • 说清楚为什么这些键在空白会话里可能是 <none>,但在有 LSP 的 buffer 里又会自动出现。
  • 为 Day 13 的日常编辑闭环打底,让“找文件 -> 跳代码 -> 改名字 -> 看诊断”真正串起来。

前置回顾
#

  • Day 009 学的是 Neovim 作为现代宿主:
    • :checkhealth
    • stdpath()
    • provider / terminal / config
  • Day 010 学的是 LazyVim 的 leader 分组和默认工作流。
  • Day 011 学的是:
    • files
    • grep
    • buffers
    • recent
    • explorer

Day 012 是一次非常关键的推进:

  • Day 011 解决的是“怎么找到文件或文本”
  • Day 012 解决的是“进到代码以后,怎么按代码结构导航”

典型场景
#

  • 你已经打开一个 Lua / TypeScript / Python 文件,想跳到某个函数或变量的定义。
  • 你想知道这个符号在哪些地方被引用。
  • 你想安全地改一个变量名,而不是全局纯文本替换。
  • 你看到一个 API,不确定它的签名或说明,想先看 hover。
  • 你知道当前文件里大概有某个函数或类,但不想靠肉眼滚动找。

最小命令集
#

这次我先用本机 LazyVim 源码,再用 nvim --headless 打开一个实际能挂上 LSP 的 Lua 文件实测了一遍。

代码跳转
#

  • gd
    • Goto Definition
  • gr
    • References
  • gI
    • Goto Implementation
  • gy
    • Goto Type Definition
  • gD
    • Goto Declaration

说明与动作
#

  • K
    • Hover
  • gK
    • Signature Help
  • <leader>ca
    • Code Action
  • <leader>cr
    • Rename
  • <leader>cR
    • Rename File
  • <leader>cl
    • Lsp Info

符号与结构
#

  • <leader>ss
    • LSP Symbols
  • <leader>sS
    • LSP Workspace Symbols
  • [[
    • Prev Reference
  • ]]
    • Next Reference

它是怎么用的
#

第一层:grepLSP 不是一回事
#

这层必须先分开。

grep
#

适合:

  • 你知道某段文本长什么样
  • 但不确定它是不是符号
  • 或者根本不依赖语言语义

比如:

  • 找字符串字面量
  • 找注释标记
  • 找配置项
  • 找某个 Markdown 标题

优先:

  • <leader>/
  • <leader>sg
  • <leader>sw

LSP
#

适合:

  • 你处理的是“代码符号”
  • 想按语言语义去找定义、引用、实现、类型

比如:

  • 一个函数定义在哪
  • 一个变量被谁引用
  • 一个方法的实现在哪
  • 一个符号应该如何 rename

优先:

  • gd
  • gr
  • gI
  • gy
  • <leader>cr
  • <leader>ss

所以 Day 12 的第一反应应该是:

  • 我知道的是文本:
    • grep
  • 我知道的是代码符号:
    • LSP

第二层:LSP 键位是“按 buffer 和能力动态挂载”的
#

这次本机实测到了一个非常重要的事实:

  • 在空白 headless 会话里,用 maparg()gdgrK<leader>ca 等键,结果是 <none>
  • 但在打开 C:\Users\86131\AppData\Local\nvim\lua\config\lazy.lua,并让 lua-language-server 挂上之后,再查就能看到:
    • gd => Goto Definition
    • gr => References
    • K => Hover
    • <leader>ca => Code Action
    • <leader>cr => Rename
    • <leader>ss => LSP Symbols

这说明:

  • 这些不是普通的全局死键位
  • 它们是 LazyVim 根据当前 buffer 的 LSP 能力,动态给你挂上的

这个心智非常重要。
否则你很容易误解成:

  • “为什么有时候能按,有时候不能按?”

更准确的理解是:

  • 当前文件没有 LSP
  • 或当前 server 不支持这个能力
  • 那这个键就不会以同样方式出现

第三层:definition / declaration / implementation / type definition 要分清职责
#

这几个名字最容易一起混。

gdGoto Definition
#

最常用。

适合:

  • 你在调用点
  • 想跳到真正实现或定义的位置

这是 Day 12 最该练顺的一个键。

gDGoto Declaration
#

有用,但不如 gd 高频。

更适合:

  • 你想看声明位置
  • 某些语言里声明和定义分开

日常主线里先记住:

  • 大多数时候先试 gd
  • 真需要声明层再试 gD

gIGoto Implementation
#

适合:

  • 接口 / 抽象 / 方法覆盖这类场景
  • 你不想看类型声明,而是想看“具体实现在哪”

gyGoto Type Definition
#

适合:

  • 你更关心这个值或对象“属于什么类型”
  • 而不是它当前符号本身的实现

最稳的记法不是背英文全称,而是按问题来想:

  • 这东西在哪定义的:
    • gd
  • 这东西在哪声明的:
    • gD
  • 这东西具体是谁实现的:
    • gI
  • 这东西属于什么类型:
    • gy

第四层:grrename 构成“理解影响面 -> 再改名”的闭环
#

Day 12 很实用的一条链路是:

  1. gr
  2. 看引用范围
  3. 再决定是否 <leader>cr

这和 Day 005 的纯文本替换很不一样。

gr
#

适合:

  • 先看一个符号“被谁用了”
  • 理解影响面

<leader>cr
#

适合:

  • 确认这真的是同一个语义符号
  • 再做重命名

这比直接 %s/old/new/g 稳得多,因为:

  • 它不是纯文本匹配
  • 它是在 LSP 语义层做 rename

所以今天最值得建立的一条真实习惯是:

  • gr
  • <leader>cr

第五层:KgK<leader>ca 是“理解和修补”的三个高频入口
#

K:Hover
#

适合:

  • 你先想知道这个符号是什么
  • 而不是立刻跳走

典型场景:

  • API 不熟
  • 想先看文档说明
  • 想看推断出来的类型信息

gK:Signature Help
#

适合:

  • 函数调用中
  • 你想看参数签名

<leader>ca:Code Action
#

适合:

  • 当前代码有可修复动作
  • 想看 LSP 提供的操作建议

比如:

  • quick fix
  • import 相关动作
  • 某些 server 提供的修复建议

这组键更像:

  • K
    • 先理解
  • gK
    • 看参数
  • <leader>ca
    • 做动作

第六层:symbols 解决的是“在结构里找”,不是“在文本里搜”
#

<leader>ss
#

适合:

  • 在当前文件里找函数、类、方法、变量等结构化符号

<leader>sS
#

适合:

  • 在整个工作区里按符号找

这组键和 grep 的差别很大:

  • grep
    • 你按文本找
  • symbols
    • 你按代码结构找

如果你在一个 Lua 配置文件里,只是想快速跳到某个函数或配置块,<leader>ss 往往比纯文本搜索更顺手。

第七层:Day 12 最稳定的练习场景是本机 Lua 配置
#

这次本机我额外确认到:

  • lua-language-server 已安装在 Mason 中
  • C:\Users\86131\AppData\Local\nvim\lua\config\lazy.lua 打开后,当前 buffer 能挂上 1 个 LSP client
  • 在这个 buffer 里,gdgrK<leader>ca<leader>cr<leader>ss 都能被实测出来

所以 Day 12 最稳的练习场景就是:

  • 直接拿你的 LazyVim 配置目录下的 Lua 文件练

这比拿一个没有 LSP 支持的普通文本文件练要稳定得多。

常见操作套路
#

套路 1:先用 Day 11 找到文件,再用 gd
#

场景:

  • 你已经能用 <leader><space><leader>/ 把目标文件找到
  • 现在要进文件里继续往下走

做法:

  1. 先开目标文件
  2. 把光标放到符号上
  3. 先试 gd

如果 Day 12 只记一个导航键,先记 gd

套路 2:先 gr 看影响面,再 <leader>cr
#

场景:

  • 你想改变量名、函数名、字段名

做法:

  1. 光标放到符号上
  2. 先按 gr
  3. 确认引用范围没问题
  4. 再按 <leader>cr

这是 Day 12 里最值得替代纯文本替换的一条工作流。

套路 3:不确定是什么,先 K
#

场景:

  • 看不懂一个 API
  • 不确定返回值或说明

做法:

  • 先按 K

比起立刻跳定义,有时候先 hover 更省上下文切换成本。

套路 4:想找当前文件结构,先 <leader>ss
#

场景:

  • 文件已经打开
  • 你只是想在它的结构里快速跳

做法:

  • <leader>ss

这时不用先 grep,也不用开 explorer。

套路 5:键位没反应时,先判断是不是“当前 buffer 没有这个能力”
#

场景:

  • 你按了 gd<leader>ca
  • 结果没反应或没有对应映射

做法:

  1. 先确认当前是不是代码文件
  2. 先确认当前 buffer 是不是挂了 LSP
  3. <leader>clLsp Info
  4. 再看:
    • :checkhealth vim.lsp
    • :Mason

不要直接把它理解成“键坏了”。

环境差异:vim / nvim / LazyVim
#

Vim:只有文本层,不负责语义导航
#

Vim 当然也能:

  • 搜文本
  • 用 tags
  • 用 quickfix

但 Day 12 这组能力的核心是语言服务器语义,不是传统文本编辑层。

Neovim:提供内建 LSP 能力
#

Neovim 官方 lsp.txt 里已经把这些基础能力定义清楚了:

  • vim.lsp.buf.definition()
  • vim.lsp.buf.references()
  • vim.lsp.buf.implementation()
  • vim.lsp.buf.type_definition()
  • vim.lsp.buf.hover()
  • vim.lsp.buf.code_action()
  • vim.lsp.buf.rename()

Day 12 的底层能力其实来自这里。

LazyVim:把它们组织成默认工作流
#

本机当前的 LazyVim 默认把这些能力组织成:

  • gd
  • gr
  • gI
  • gy
  • gD
  • K
  • <leader>ca
  • <leader>cr
  • <leader>ss

而且还是按 server 能力动态挂载的。

所以更准确的关系是:

  • Vim 提供编辑语言
  • Neovim 提供 LSP 宿主能力
  • LazyVim 提供默认键位和工作流编排

今日练习(5-10 分钟)
#

练习任务 A:在本机 Lua 配置里练 gd / K / gr
#

  1. 打开:
    • C:\Users\86131\AppData\Local\nvim\lua\config\lazy.lua
  2. 把光标停在一个函数或模块名上
  3. 依次试:
    • K
    • gd
    • gr

目标:

  • 感受到“先看说明”和“直接跳定义”的区别。

练习任务 B:练一次结构化 rename
#

  1. 在有 LSP 的 Lua 文件里找一个明显的局部符号
  2. 先按 gr
  3. 再按 <leader>cr
  4. 看 rename 影响范围

目标:

  • 建立“先看引用,再 rename”的工作流。

练习任务 C:练 symbols
#

  1. 打开一个稍微长一点的 Lua 文件
  2. 试:
    • <leader>ss
    • <leader>sS
  3. 对比它和 Day 11 里 <leader>/ 的感觉差异

目标:

  • 说清楚“按文本找”和“按结构找”的区别。

今日问题与讨论
#

我的问题
#

  • 暂无。本节先把 LSP 语义导航和默认入口搭起来,后续问答直接补到这里。

外部高价值问题
#

问题 1:为什么我在空白会话里查 gd<none>,进了 Lua 文件又有了?
#

  • 问题:
    • 为什么这些看起来像默认键位的东西,不是全局一直存在?
  • 简答:
    • 因为 LazyVim 会根据当前 buffer 的 LSP client 和 server 能力,动态挂载这些键位。
  • 场景:
    • 空白 headless 会话里没有 LSP;打开 lazy.lua 这种 Lua 文件,并挂上 lua-language-server 之后,对应键位才会出现。
  • 依据:
    • 本机 maparg() 空会话实测为 <none>
    • 本机打开 C:\Users\86131\AppData\Local\nvim\lua\config\lazy.lua 后再测,出现 Goto DefinitionHoverRename 等描述
    • 本机 LazyVimlua/lazyvim/plugins/lsp/init.luakeymaps.lua
  • 当前结论:
    • Day 12 这组键要理解成“LSP buffer 工作流”,不是普通全局快捷键。
  • 是否需要后续回看:

问题 2:grep 已经很强了,为什么还要学 gd / gr / rename
#

  • 问题:
    • 既然 Day 11 已经能快速搜文本,Day 12 的 LSP 又额外带来什么?
  • 简答:
    • 因为文本搜索只知道“字符长得像”,LSP 才知道“这个符号在代码里是什么意思”。
  • 场景:
    • 同名变量、重载方法、不同作用域、类型定义、引用范围,这些都不是纯文本搜索能稳定处理的。
  • 依据:
    • Day 011 的 grep 工作流
    • Neovim 官方 lsp.txt
    • 本机 LazyVim 默认 LSP keymaps
  • 当前结论:
    • 文本搜索和 LSP 不是替代关系,而是两条不同层级的导航方式。
  • 是否需要后续回看:

问题 3:gdgDgIgy 这么多“跳”,到底先按哪个?
#

  • 问题:
    • 这些名字看起来都像跳转,容易记混。
  • 简答:
    • 先把 gd 练成第一反应,其他三个按具体问题再分化。
  • 场景:
    • 大多数真实场景里,你最先想知道的是“定义在哪”。
  • 依据:
    • 本机 LazyVim 默认 LSP keymaps
    • Neovim 官方 lsp.txt
  • 当前结论:
    • 日常优先级先记成:
      • gd
      • gr
      • K
      • <leader>cr
    • 再逐步补:
      • gD
      • gI
      • gy
  • 是否需要后续回看:

问题 4:为什么 rename 应该先看 gr
#

  • 问题:
    • 能直接 rename,为什么还要先多按一步?
  • 简答:
    • 因为你先看引用范围,才知道这次改名会波及哪些地方。
  • 场景:
    • 特别是在陌生代码、配置仓库或命名不稳定的代码里,先看引用更稳。
  • 依据:
    • 本机 gr => References
    • 本机 <leader>cr => Rename
    • Day 005 / Day 008 的“先找影响面,再改”的主线心智
  • 当前结论:
    • gr -> <leader>cr 是 Day 12 最值得练顺的一条闭环。
  • 是否需要后续回看:

问题 5:我本地已经有 clang / clangd,想给 Neovim 配 C/C++ LSP,应该怎么做?
#

  • 问题:
    • 本机 clang --versionclangd --version 都已经能跑,当前又是在 LazyVim starter 上,最稳的接法是什么?
  • 简答:
    • 对当前这套 LazyVim,最稳的做法不是从零手搓 lspconfig,而是启用官方 clangd extra,再显式把 clangd 设成 mason = false,让它直接走你系统里的 clangd
  • 场景:
    • 你已经有本机 LLVM / clang 工具链,不想再额外装一份 Mason 版 clangd,但又想拿到 C/C++ 的 definition、references、hover、rename、source/header 切换这些 LSP 能力。
  • 依据:
    • 本机 clang --version
    • 本机 clangd --version
    • 本机 nvim --headless "+lua print(vim.fn.executable('clangd'))" +qa 输出 1
    • 本机 C:\Users\86131\AppData\Local\nvim-data\lazy\LazyVim\lua\lazyvim\plugins\extras\lang\clangd.lua
    • 本机 C:\Users\86131\AppData\Local\nvim-data\lazy\LazyVim\lua\lazyvim\plugins\lsp\init.lua
    • LazyVim 官方 Clangd extra 文档
    • clangd 官方 compile_commands.json / compile_flags.txt 文档
  • 当前结论:
    • 对你当前机器,推荐新建 C:\Users\86131\AppData\Local\nvim\lua\plugins\lang-clangd.lua,内容类似:
LUA 代码 · 共 13 行
return {
  { import = "lazyvim.plugins.extras.lang.clangd" },
  {
    "neovim/nvim-lspconfig",
    opts = {
      servers = {
        clangd = {
          mason = false,
        },
      },
    },
  },
}
  • 如果你不介意直接用 Mason 版 clangd,那最省事的是直接 :LazyExtras 启用 lang.clangd,不用再补 mason = false
  • 只把 clangd 接进编辑器还不够。对真实项目,项目根最好还要有:
    • compile_commands.json
    • compile_flags.txt
  • 否则 clangd 往往会退回到类似 clang foo.cc 的默认命令,真实项目里很容易出现 #include 找不到、标准版本不对、诊断大量飘红。
  • 如果你用的是 CMake,最稳的是先生成:
BASH 代码 · 共 1 行
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=1
  • 如果是简单小项目,没有构建系统,可以先在项目根手写一个 compile_flags.txt,把 -std=-I 等参数一行一个写进去。
  • 是否需要后续回看:

常见误区或易混点
#

  • 误区 1:LSP 等于“更高级的 grep”。
    • 不对。它是语义层,不是文本层。
  • 误区 2:gd 这种键应该在所有 buffer 里都能按。
    • 不对。它依赖当前 buffer 的 LSP 与 server 能力。
  • 误区 3:rename 可以直接替代所有替换命令。
    • 不对。它更适合语义符号,不适合所有纯文本修改。
  • 误区 4:gdgDgIgy 必须一口气全背熟。
    • 不用。先把 gd 练顺,再逐步分化。
  • 误区 5:一进入代码文件,就只该用 LSP,不该再用搜索。
    • 不对。文本搜索和语义导航各有边界。

扩展内容
#

  • <leader>cl
    • Lsp Info,排查当前 buffer 的 LSP 状态很有用。
  • <leader>cR
    • Rename File,依赖 server 的文件操作支持。
  • [[ / ]]
    • 在支持 documentHighlight 的 buffer 里,可以跳前后引用。
  • <leader>cc / <leader>cC
    • Codelens 相关入口,但不是每个 server / 文件都高频。
  • :Mason
    • 适合后面排查 server 安装情况。

今日小结
#

  • Day 12 的关键不是“又多了几个键位”,而是第一次真正把“语义导航”接进工作流:
    • gd
    • gr
    • K
    • <leader>ca
    • <leader>cr
    • <leader>ss
  • 同时还要建立一个非常关键的事实:
    • 这些键位不是全局死映射
    • 它们是按当前 buffer 的 LSP 能力动态出现的
  • 这层一旦立住,你后面做代码阅读、重命名、定位影响面就会稳定很多。

明日衔接
#

  • 下一天进入 LazyVim 日常编辑闭环
  • 到那时,会把前面的东西真正串成一个真实工作流:
    • 找文件
    • 跳代码
    • 看引用
    • 改名字
    • 看诊断
    • 保存 / 回退 / 继续切上下文

复习题
#

  1. grepLSP 的根本区别是什么?
  2. 为什么 gd 这类键在空白会话里可能是 <none>
  3. gdgDgIgy 分别更适合回答什么问题?
  4. 为什么 Day 12 推荐先 gr<leader>cr
  5. <leader>ss 和 Day 11 的 <leader>/ 分别在找什么?
Vim 14 天 - 这篇文章属于一个选集。
§ 12: 本文