11-导航源文件
第 11 章 导航源文件
在之前的章节中,我们学习了许多在单个缓冲区内以及在打开的标签页和窗口之间导航的不同方法。本章将详细介绍在源文件之间导航的不同方法。
11.1. 转到定义 (Go To Definition)
在我看来,“转到定义”是语言服务器带给我们的最有价值的功能。主流 IDE 对编译型语言支持这个功能已经有很长时间了,但是动态类型语言——比如我钟爱的 Python——对于静态分析来说一直是个地狱,这类功能往往很不稳定。
作为有史以来命名最好的编辑器功能之一,“转到定义”将你的光标从当前所在的任何关键字跳转到该关键字定义的位置,无论它在哪个文件中。
最常见的情况是,当我在查看一个函数的调用点并想查看函数本身时使用这个功能。简单地按一下 gd
(转到定义也是 LazyVim 中有史以来最令人难忘的快捷键之一)就能带我到那里。
译者注:
gd
(go definition)。
根据我正在编辑的语言的 LSP 的好坏程度,这通常甚至允许我跳转到库文件或第三方模块的类型声明中,以便我能真正了解发生了什么。
转到定义是上下文相关的,但通常会完全按照你的预期进行。如果你正在查看一个变量,gd
将跳转到该变量初始化的位置。如果你的光标在一个类名上,它将跳转到该类定义的位置。
通常,一旦你跳转到定义并从该文件中了解到你需要的信息后,你会立即想要跳回到你开始的地方。你可以使用 Control-o
轻松做到这一点,正如我们在第 3 章讨论的那样(并且 Control-i
可以在你的跳转历史中向前移动)。
11.2. 转到引用 (Go To References)
“转到定义”命令的反向操作是“转到引用”。如果你正在查看一个函数、变量、类型等,并且想查看该变量被访问的所有位置,请使用 gr
命令。
译者注:
gr
(go references)。
与定义或声明不同,对于给定的单词通常会有多个引用(一个孤立的变量确实是一个无用的变量)。所以当你输入 gr
时,它通常不会立即跳转到一个位置。相反,它会弹出一个选择器视图,显示光标下单词的所有引用,并带有选择器一贯的预览和过滤的便利性。
通常需要在每个引用处执行某些操作——例如重命名或添加参数等等。你可以通过再次按 gr
或使用恢复你之前选择器搜索的 <Space>sR
快捷键来不断显示选择器。然而,使用我们在第 10 章学到的 Trouble 列表通常更有用。
为此,像往常一样使用 gr
在选择器中显示引用。然后使用 Alt-a
选择所有结果,可能用 <Tab>
取消选择某些结果。现在使用 Enter
在 Quick Fix 中打开选定的行,或使用 Control-t
在 Trouble 中打开它们。记住,Control-q
是在 Quick Fix 中打开所有匹配项的快捷方式。
11.3. 上下文相关帮助
大多数非模式编辑器在你将鼠标悬停在单词或符号上时会显示一些帮助或“悬停”文本。这些文本的数量和价值因 LSP 而异,但通常包括函数签名和光标下单词的文档。
可能可以设置 Neovim 在悬停时显示帮助文本,但是当 LazyVim 在键盘上有如此惊人的导航功能时,你为什么要将手移到鼠标上呢?相反,请使用(Shift 后的)K
快捷键。是的,K
是一个相当愚蠢的助记符,但 H
和 ?
已经被占用了。
K
代表“keywordprog”,这是一个遗留的 Vim 概念,在现代世界中已被语言服务器取代。所以 LazyVim 重新利用了这个快捷键。
11.4. 列出符号 (Listing Symbols)
另一个方便的 LSP 功能是搜索当前文件或项目中的所有符号。如果你正在编辑一个特别长的文件,并且需要跳转到一个离光标不太近的函数,你可能会使用 <Space>ss
命令(助记符是“search symbols”,搜索符号)。正如双 s
所暗示的,这预计会是一个相当常见的操作。
弹出的对话框现在应该相当熟悉了,因为它是通常的选择器:
图 49. Picker 符号
所以你已经知道如何使用它了。然而,我想提醒你几个让它更有用的 Picker 技巧:
大多数时候当我使用这个符号选择器时,我只关心函数,或者有时是类。所以上面截图中散布的字段和属性只是干扰。确实可以配置选择器只显示某些类型的符号,但我更喜欢一个快速技巧,让我能将其范围缩小到只有函数:输入单词 function
(或其部分)。
由于选择器在结果的第二列包含了单词“function”,它很高兴地过滤掉了所有不包含该单词的行。很方便。
更好的是,我可以在单词“function”之后输入一个空格,告知选择器在第一列重新执行后续搜索。所以“func api”会过滤出所有名称中包含单词“api”的函数。
我的第二个技巧是不要忘记 Control-q
和 Control-t
快捷键,用于将选择器结果转储到 Quick Fix 或 Trouble 列表中。它为你过滤的任何符号生成了一个快速粗略的目录。
如果你想搜索整个项目中的所有符号,请使用“但更大”的助记符。<Space>sS
将执行这样的搜索。但是请注意,并非所有 LSP 都支持工作空间符号搜索。有些只在当前打开的文件中搜索,即使是那些完全支持工作空间符号搜索的 LSP 中,也有许多慢得无法使用。
译者注:
<Space>ss
(search symbols in document),<Space>sS
(search symbols in workspace)。
11.5. Trouble 也有符号大纲
你也可以使用 Trouble 插件打开一个符号大纲。快捷键是 <Space>cs
,你可能很难找到它,因为它在 Code
(代码)菜单而不是 Search
(搜索)菜单中。与大多数 Trouble 窗口不同,它默认在右侧边栏打开。它创建了一个漂亮的树状视图,你甚至可以使用我们在第 9 章讨论的折叠快捷键来折叠和展开树节点。
译者注:
<Space>cs
(code symbols)。
图 50. Trouble 符号
你可以使用与调整窗口大小通常使用的相同快捷键(<Space>w<
和 <Space>w>
)来调整 Trouble 窗口的大小。当你将光标移到 Trouble 窗口上时,它所在的符号会自动滚动到视图中。
使用 Trouble 窗口最快的方法是使用 Seek 模式。回想一下,Seek 模式可以跳转到任何当前可见的窗口,这包括 Trouble。所以如果我当前正在编辑上面的文件,并且我的光标当前在文件末尾附近,我可以使用 spub
进入 Seek 模式并搜索字符“pub”。这将在 Trouble 窗口的 publicKeyToken
上放置一个标签。如果我按下那个标签,我的光标会跳转到 trouble 窗口,并且我的编辑器窗口会立即滚动到相应的函数。现在我只需按 Enter
将光标移回我正在编辑的文件。
11.6. 上下文 (Context)
nvim-treesitter-context
extra 是一个了解你在当前文件中位置的有用方式。它使用 treesitter 来确定你所在的函数和类型,然后将定义这些类型的行固定在编辑器顶部。像往常一样,通过访问 :LazyExtras
并在包含 nvim-treesitter-context
的行上按 x
来启用它。
译者注: 这个插件通常在屏幕顶部显示当前代码所在的上下文(如函数名、类名),即使定义行已经滚动出屏幕。
这个插件会跟踪你的光标当前所在的类或函数。如果函数或类型定义太长以至于签名滚动出屏幕,它会很贴心地将该签名复制到代码窗口的第一行或几行,并用稍微不同的背景颜色高亮显示。
用参考图片更容易描述,考虑这个截图:
图 51. Treesitter 上下文
在这张图片中,前两行(稍微带有底纹)提供了上下文,而不是缓冲区的一部分。第一行告诉我我在 DexieApiClient
类中,第二行告诉我当前正在查看该类中的 forceAddMemberToRealm
方法。
特别注意相对行号列。class DexieApiClient
行在我的当前光标位置上方 109 行,而 async forceAddMemberToRealm
行在其上方 27 行。相比之下,该函数的第一个可见行仅在我的当前光标位置上方 13 行。
效果相当微妙,但进入这个上下文部分的定义往往正是你编码时所需要的。如果它们能放在一行上,我可以看到函数签名和返回类型。你真的不会注意到你有多经常需要向上滚动查看函数签名中的变量名,直到你不再需要这样做!而且如果你确实需要向上滚动到签名,只需输入相对行号后跟 k
,无需搜索即可到达那里。
如果你需要临时禁用上下文,请使用快捷键 <Space>ut
。我们还没怎么见过 <Space>u
菜单,你可以在那里切换各种用户界面效果。这主要是因为默认的用户界面配置得足够好,你不需要经常更改它!
译者注:
<Space>ut
(UI Toggle Context)
11.7. 使用标记 (书签) 导航
你已经知道如何使用 Control-o
和 Control-i
在你的历史记录中导航,以及使用各种各样的动作高效地在文档中跳转。
Vim 还包括一个“书签”功能,尽管它被称为“标记”(marks),我猜是因为 m
字符在 Vim 键映射上仍然是空闲的。
译者注: Mark (标记) 是 Vim 内建功能,允许你在文件中设置命名位置以便快速跳转。
标记是 Vim 内置的,LazyVim(像往常一样)添加了一些小的改进。
很像我们在第 8 章介绍的寄存器,标记可以分配给字母表中的每个字母。此外,某些标点符号代表你可以跳转到但不能设置的特殊系统设置标记。
要在某一行设置标记,请在任何字母字符前加上字母 m
。所以 ma
将在当前行设置标记 a
。你可以通过左侧边栏中的 a
字符来判断这一行被标记为 a
:
图 52. 标记在符号列中
现在我可以从当前文件的任何地方使用单引号后跟 a
(即 'a
)跳转到标记为 a
的行。
译者注:
m<小写字母>
: 在当前位置设置一个文件内标记 (local mark)。'<小写字母>
: 跳转到该文件内标记所在的行首。`<小写字母>
: 跳转到该文件内标记所在的精确位置(行和列)。
我不常用这个,因为其他工具在文件内导航通常比手动设置标记更有用。然而,如果我用大写字母标记了行(例如 mA
),我就能使用 'A
从任何当前打开的文件跳转到该标记。
译者注:
m<大写字母>
: 设置一个全局标记 (global mark),可以在不同文件间跳转。'<大写字母>
: 跳转到设置该全局标记的文件和行首。`<大写字母>
: 跳转到设置该全局标记的文件和精确位置。
所以本质上,你可以在你打开的每个文件中拥有最多 26 个局部标记,以及可以从任何文件访问的 26 个全局标记。
方便的是,如果我只输入一个单引号(在普通模式下),LazyVim 会弹出一个菜单,列出当前所有可供跳转的标记:
图 53. 标记菜单
这个列表显示了我在这个文件中设置的小写 a
标记,几个我可以用标点符号跳转的系统标记(注意右侧每个标记的描述,这样你就不必记住它们了),两个我用来跳转到我的 kitty 和 fish 配置文件的全局标记,以及十个编号标记。
我发现编号标记有点没用。它们基本上指向你上次关闭 Neovim 时的文件和光标位置。除非我正在编辑提交信息或临时实例中的拉取请求描述,否则我不常关闭 Neovim,所以我的编号标记大多只是那些临时文件。如果我需要回到我之前的位置,<Space>qs
快捷键来恢复会话通常比编号标记更有用。
译者注: 编号标记
0-9
自动记录最近关闭的文件位置。'0
是最近关闭的文件位置,'1
是次近的,以此类推。
当你按下单引号键时弹出的菜单通常足以找到标记,但你也可以使用 <Space>sm
快捷键在选择器中搜索标记。我通常没有足够多的活动标记让这个变得有用,但如果你设置了很多全局和局部标记,并且记不清哪个字母与某个特定标记相关联,使用选择器搜索你标记行的内容可能会有所帮助。
译者注:
<Space>sm
(Search Marks)。
一旦你设置了一个标记,你最终会被一个问题困扰:“我该如何摆脱它?”删除标记可能和“如何退出 Vim”一样是最常见的查询之一!没有删除标记的快捷键。相反,你需要使用命令 :delmarks <标记>
来删除给定的标记。这可以缩短为 :delm <标记>
。所以要摆脱这个文件中的 a
标记,我使用了命令 :delmarks a
。你不必在标记行上才能删除标记。
在命令模式下,标记可以在范围中代替行号使用。例如,如果你想将标记 a
和标记 b
之间的文本写入文件,你可以执行 :'a,'bwrite somefile.txt
。如果你在选中文本时看到冒号行前面有 '<,'>
,那是因为 '<
和 '>
代表最近一次可视选择的开始和结束。所以,与其手动设置 ma
和 mb
,你可以可视化地选择你想要写入的内容,让这些标记为你预先填充好。
你也可以使用 '<
和 '>
跳转到最近一次选择的开始或结束,即使它此后已被取消选择。
我经常使用的另一个符号标记是 '.
,它跳转到我上次插入或更改文本的地方。这有时比一系列 Control-o
按键更快。
11.8. 总结
在本章中,我们学习了如何使用跳转到定义和引用,以及各种“文档符号”插件来导航代码文件。
我们看到了 LazyVim 如何为我们提供当前文档位置的上下文,以及如何查找光标下符号的文档。
最后,我们介绍了 Vim 标记,这是一种更手动的方式来跟踪你可能想要跳转到的位置。
在下一章中,我们将学习在当前文件和全局项目中搜索文本的所有知识。