开发
Nvim :help
页面,生成自 源代码 使用 tree-sitter-vimdoc 解析器。
本参考描述了用于开发 Nvim 应用程序或 Nvim 本身的的设计约束和指南。请参阅
dev-arch 以了解 Nvim 的架构和内部概念的讨论。
最重要的内容排在最前面(大致上)。有些项目存在冲突,这是故意的。必须找到平衡点。
Nvim 的 Neo 部分应该使其成为更好的 Vim,而不会变成一个完全不同的编辑器。
在品味方面,优先考虑 Vim/Unix 传统。如果不存在相关的 Vim/Unix 传统,请考虑“常见情况”。
可以添加的功能没有限制。根据 (1) 用户的要求、(2) 实现所需的工作量和 (3) 实际执行的人员来选择新功能。
向后兼容性是一项功能。尤其是 RPC API 永远不应该中断。
未记录的功能是无用的功能。新功能的补丁必须包含文档。
文档应该全面且易懂。请使用示例。
不要使文本过长。较少的文档意味着更易于查找某个项目。
保持 Nvim 体积小巧且运行速度快。这直接影响着多功能性和易用性。
计算机每年都在变得越来越快、越来越大。Vim 也可以增长,但增长速度不能快于计算机的增长速度。保持 Vim 在旧系统上可用。
许多用户经常从 shell 启动 Vim。启动时间必须很短。
命令必须高效运行。它们消耗的时间必须尽可能短。有用的命令可能需要更长的时间。
不要忘记有些人通过缓慢的连接使用 Vim。尽量减少通信开销。
Vim 是其他组件中的一个组件。不要将其变成一个庞大的应用程序,但要使其与其他程序良好协作(“可组合性”)。
源代码不应变得混乱。它应该是可靠的代码。
以有用的方式使用注释!引用函数名和参数名并不好。请解释它们的作用。
应简化移植到其他平台的过程,无需对平台无关代码进行过多更改。
使用面向对象的理念:将数据和代码放在一起。尽量减少知识传播到代码的其他部分。
Nvim 不是操作系统;相反,它应该与其他工具组合在一起或作为组件托管。Marvim 曾经说过:“与 Emacs 不同,Nvim 不包含厨房水槽……但它很适合水管工。”
Nvim 的主要目标是允许扩展编辑器,而无需在核心方面具有特殊知识。一些核心功能被委派给作为外部脚本实现的“提供者”。
示例
1. 在 Vim 源代码中,剪贴板逻辑占用了超过 1k 行的 C 源代码 (ui.c),以执行现在可以使用 shell 命令(如 xclip 或 pbcopy/pbpaste)完成的两项任务。
2. Python 脚本支持:Vim 有三个文件专门用于嵌入 Python 解释器:if_python.c、if_python3.c 和 if_py_both.h。这些文件加起来大约有 9.5k 行 C 源代码。相比之下,Nvim Python 脚本由一个在 ~2k 行 Python 代码中实现的外部主机进程执行。
提供者框架从 C 调用 Vimscript。它由 eval.c 中的两个函数组成
eval_call_provider({name}
, {method}
, {arguments}
, {discard}
):使用 {method}
和 {arguments}
调用 provider#{name}#Call
。如果 {discard}
为真,则提供者返回的任何值都将被丢弃,并返回空值。
eval_has_provider(
{name}
):检查
g:loaded_{name}_provider
变量,该变量必须由提供者脚本设置为 2,以指示它“已启用且工作”。由
has() 调用以检查功能是否可用。
例如,Python 提供者由“autoload/provider/python.vim”脚本实现,该脚本仅在找到有效的外部 Python 主机时才将 g:loaded_python_provider
设置为 2。然后 has("python")
反映 Python 支持是否有效。
provider-reload有时 GUI 或其他应用程序可能希望强制提供者“重新加载”。要重新加载提供者,请取消定义其“已加载”标志,然后使用
:runtime 重新加载它
:unlet g:loaded_clipboard_provider
:runtime autoload/provider/clipboard.vim
“直说”。在所有文档(docstrings、用户手册、网站资料、新闻稿……)中避免使用含糊不清的口语化表达方式。不要拐弯抹角。个性和风格(适度使用)是受欢迎的——但总的来说,要以读者的宝贵时间和精力为优化目标:“简洁明了”。
优先使用主动语态:“Foo 执行 X”,而不是“X 由 Foo 执行”。
编写 docstrings(而不是内联注释)时,使用现在时态(“获取”),而不是祈使句(“获取”)。这往往会减少歧义并提高清晰度,因为它描述的是“什么”,而不是“如何”。
✅ OK:
/// Gets a highlight definition.
❌ NO:
/// Get a highlight definition.
避免以“The”或“A”开头 docstrings,除非为了避免歧义。这是一个视觉辅助工具,可以减少噪音。
✅ OK:
/// @param dirname Path fragment before `pend`
❌ NO:
/// @param dirname The path fragment before `pend`
Vim 的差异
使用一致的语言。
帮助标签中的“terminal”始终表示“嵌入式终端模拟器”,而不是“用户主机终端”。
使用“tui-”作为与主机终端相关的帮助标签的前缀,并在散文中尽可能使用“TUI”。
有关 Lua 文档应放置位置的大致指南
Nvim API 函数 vim.api.nvim_*
应在 api.txt
中。
如果模块很大,并且与通用和低级 Lua 功能无关,那么它很可能需要单独存放。例如:treesitter.txt
否则,请将其添加到 lua.txt
中
严格的“vimdoc”子集
使用列表(像这样!),以“-”或“•”作为前缀,用于你不希望自动换行的相邻行。列表始终以“流动”布局(软换行)呈现,而不是以传统 :help 文档中常见的预格式化(硬换行)布局呈现。
使用空行分隔内容块(段落)。
不要在随机位置使用缩进——这会阻止页面使用“流动”布局。如果你需要一个预格式化的部分,请将其放在以“>”开头的
help-codeblock 中。
参数和字段以 {foo}
形式记录。
可选参数和字段以 {foo}?
形式记录。
Nvim API 文档位于源代码中,作为函数定义中的 docstrings(doc 注释)。
api :help 是从 src/nvim/api/*.c 中定义的 docstrings 生成的。
Docstring 格式
行以 ///
开头
特殊标记以 @
后跟标记名称开头:@note
、@param
、@return
支持 Markdown。
标签以 [tag]()
形式编写。
引用以 [tag]
形式编写
对代码示例使用 ```。代码示例可以注释为 vim
或 lua
示例:
nvim_open_win() 的帮助是从 src/nvim/api/win_config.c 中定义的类似于以下 docstring 生成的
/// Opens a new window.
/// ...
///
/// Example (Lua): window-relative float
///
/// ```lua
/// vim.api.nvim_open_win(0, false, {
/// relative='win',
/// row=3,
/// col=3,
/// width=12,
/// height=3,
/// })
/// ```
///
/// @param buffer Buffer to display
/// @param enter Enter the window
/// @param config Map defining the window configuration. Keys:
/// - relative: Sets the window layout, relative to:
/// - "editor" The global editor grid.
/// - "win" Window given by the `win` field.
/// - "cursor" Cursor position in current window.
/// ...
/// @param[out] err Error details, if any
///
/// @return Window handle, or 0 on error
Lua docstrings
dev-lua-docLua 文档位于源代码中,作为函数定义中的 docstrings。
lua-vim :help 是从 docstrings 生成的。
Docstring 格式
支持 Markdown。
标签以 [tag]()
形式编写。
引用以 [tag]
形式编写
对代码示例使用 ```。代码示例可以注释为 vim
或 lua
使用
@since <api-level>
来记录函数何时变得“稳定”的
api-level。如果
<api-level>
大于当前的稳定版本(或 0),则将其标记为“实验性”。
请参阅 scripts/util.lua 以了解 api-level 与 Nvim 版本的映射关系。
使用 @nodoc
阻止文档生成。
使用
@inlinedoc
将
@class
块内联到
@param
块中。例如
--- Object with fields:
--- @class myOpts
--- @inlinedoc
---
--- Documentation for some field
--- @field somefield? integer
--- @param opts? myOpts
function foo(opts)
end
将呈现为
foo({opts})
Parameters:
- {opts}? (table) Object with the fields:
- {somefield}? (integer) Documentation
for some field
声明为 @meta
的文件仅用于类型化和文档化(类似于“*.d.ts” typescript 文件)。
示例:
vim.paste() 的帮助是从 runtime/lua/vim/_editor.lua 中用类似于以下 docstring 装饰 vim.paste 生成的
--- Paste handler, invoked by |nvim_paste()| when a conforming UI
--- (such as the |TUI|) pastes text into the editor.
---
--- Example: To remove ANSI color codes when pasting:
---
--- ```lua
--- vim.paste = (function()
--- local overridden = vim.paste
--- ...
--- end)()
--- ```
---
--- @since 12
--- @see |paste|
---
--- @param lines ...
--- @param phase ...
--- @returns false if client should cancel the paste.
LUA STDLIB 设计指南 dev-lua
保持核心 Lua 模块
lua-stdlib 简单。避免使用复杂的 OOP 或伪 OOP 设计。插件作者只需要调用函数,而不是庞大而复杂的继承层次结构。
避免在 Nvim stdlib 中要求或返回特殊对象。普通表或值更易于序列化,更易于从文字构造,更易于检查和打印,并且本质上与所有 Lua 插件兼容。(此指南不适用于不透明的非数据对象,如 vim.cmd
。)
stdlib 函数应遵循以下常见模式
接受可迭代对象,而不仅仅是表。
注意:在某些情况下,可迭代对象没有意义,例如,spair() 按定义对输入进行排序,因此它没有理由接受可迭代对象,因为输入需要被“具象化”;它不能对“流”进行操作。
如果可能,返回一个可迭代对象(生成器)而不是表。
如果函数旨在用于
for-in 循环,则模仿 pairs() 或 ipairs() 接口。
dev-error-patterns为了向使用者传达失败,请从以下模式中选择(按优先级排序):1.
retval, errmsg
1 的特殊情况。当只有一个“不存在”的情况(例如,缓存查找,字典查找)。3. error("no luck")
对于无效状态(“绝不应该发生”),当失败是异常情况,或者在低级水平上,使用者不太可能以有意义的方式处理它时。优点是传播会自动发生,并且更难意外吞并错误。(例如,使用 uv_handle/pipe:write()
而不检查返回值很常见。)4. on_error
参数
对于异步和遍历图的“访问者”,在继续工作时可能收集到许多错误。5. vim.notify
(有时带有可选的 opts.silent
(异步,访问者^))
高级/应用程序级消息。最终用户直接调用它们。
在可能的情况下,这些模式适用于 Lua 和 API。
在接受缓冲区 ID 等时,0 表示“当前缓冲区”,nil 表示“所有缓冲区”。窗口 ID、标签页 ID 等也是如此。
任何接受回调的函数签名(例如:
table.foreach())都应将它放在最后一个参数(在 opts 之后),如果可能的话(或者总是用于“延续回调”——只调用一次的函数)。
通过将“噪声”较少的参数放在开头,提高了可读性。
与 luv 保持一致。
对于将 function(<args>, cb(<ret>))
转换为 function(<args>) -> <ret>
的未来异步库很有用。
示例
-- ✅ OK:
filter(…, opts, function() … end)
-- ❌ NO:
filter(function() … end, …, opts)
-- ❌ NO:
filter(…, function() … end, opts)
“启用”(“切换”)接口和行为
enable(…, nil)
和 enable(…, {buf=nil})
是同义词,用于控制功能的“全局”启用。
is_enabled(nil)
和 is_enabled({buf=nil})
同样查询功能的全局状态。
enable(…, {buf: number})
设置一个缓冲区本地的“启用”标志。
is_enabled({buf: number})
同样查询功能的缓冲区本地状态。
添加 API 时,请检查以下内容
你借鉴了什么先例?你的解决方案与它们相比如何?
你的新 API 是否允许未来扩展?如何?或者为什么不?
新 API 是否与现有 API 相似?我们需要弃用旧的 API 吗?
你是否在文档中交叉引用了相关的概念?
避免“相互排斥”的参数——如果需要,可以通过约束或限制。例如,nvim_create_autocmd() 具有相互排斥的“回调”和“命令”参数;但可以通过简单地不支持 Vimscript 函数名称来消除“命令”参数,并将字符串“回调”参数视为一个 Ex 命令(它可以调用 Vimscript 函数)。“缓冲区”参数也可以通过将数字“模式”视为一个缓冲区编号来消除。
避免依赖于光标位置、当前缓冲区等的函数。相反,函数应该接收一个位置参数、缓冲区参数等。
API(libnvim/RPC):公开低级内部机制,或客户端或 C 使用者需要的基本内容(例如 nvim_exec_lua()
)。
Lua 标准库 = 建立在 API 之上的高级功能。
命名极其重要:事物的名称是使用它、讨论它、搜索它、分享它……的主要接口。标准库、API 和 UI 中一致的命名有助于用户和开发人员发现并直观地理解相关概念(“家族”),并减轻认知负担。可发现性鼓励代码重用,同样避免冗余、重叠的机制,这减少了代码表面积,从而最大程度地减少了错误……
一般来说,在选择名称时,请查看先例,即查看现有的(未弃用的)函数。特别地,请参见以下内容……
dev-name-common如果可能,使用现有的通用
{verb}
名称(动作)
add:追加或插入到集合中
attach:监听某事物以获取来自它的事件(待办事项:重命名为“on”?)
call:调用一个函数
cancel:取消或关闭一个事件或交互,通常是用户发起的,并且没有错误。(与“abort” 相比,后者取消并发出错误/失败信号。)
clear:清除状态,但不销毁容器
create:创建一个新的(非平凡的)事物(待办事项:重命名为“def”?)
del:删除一个事物(或一组事物)
detach:处置附加的侦听器(待办事项:重命名为“un”?)
enable:启用/禁用功能。签名应为 enable(enable?:boolean, filter?:table)
。
eval:评估一个表达式
exec:执行代码,可能会返回一个结果
fmt:格式化
get:获取事物(通常通过查询)
inspect:提供一个高级的,通常是交互式的视图
is_enabled:检查功能是否已启用。
open:打开某事物(一个缓冲区,窗口,……)
parse:解析某事物为结构化形式
set:设置一个事物(或一组事物)
start:启动一个长期运行的进程。除了“start”明显更合适的情况外,更喜欢“enable”。
stop:“start”的逆运算。拆卸一个长期运行的进程。
try_{verb}:尽力而为的操作,失败则返回 null 或错误对象
不要使用以下弃用的动词
disable:更喜欢 enable(enable: boolean)
。
exit:更喜欢“cancel”(如果合适,也可以使用“stop”)。
is_disabled:更喜欢 is_enabled()
。
list:与“get”冗余
notify:与“print”,“echo”冗余
show:与“print”,“echo”冗余
toggle:更喜欢 enable(not is_enabled())
。
在 API 函数中使用一致的
{topic}
名称:缓冲区在任何地方都称为“buf”,而不是在某些地方称为“buffer”,而在其他地方称为“buf”。
buf:缓冲区
cmd:命令
cmdline:命令行 UI 或输入
fn:函数
hl:高亮
pos:位置
proc:系统进程
tabpage:标签页
win:窗口
不要使用以下弃用的名词
buffer:改用“buf”
callback:改用 on_foo
command:改用“cmd”
window:改用“win”
dev-name-events使用“on_”前缀为事件处理回调以及“注册”此类处理程序的接口命名(on_key)。双重性质是可以接受的,以避免对这些相关概念使用混淆的命名约定。
编辑器
事件(自动命令)在历史上被称为
{Noun}{Event}
使用此格式为 API(RPC)事件命名
nvim_{noun}_{event-name}_event
示例
nvim_buf_changedtick_event
dev-api-name使用此格式为新的 RPC
API 函数命名
nvim_{topic}_{verb}_{arbitrary-qualifiers}
不要添加新的 nvim_buf/nvim_win/nvim_tabpage API,除非你确信该概念永远不会应用于超过一个“范围”。也就是说,
{topic}
应该是作用于范围(buf/win/tabpage/global)的主题(“ns”,“extmark”,“option”……),而不是范围。相反,范围应该是一个参数(通常表现为像
nvim_get_option_value() 这样的相互排斥的 buf/win/… 标志,或者更不常见的是像
nvim_get_option_info2() 这样的
scope: string
字段)。
示例:nvim_get_keymap('v')
在全局上下文中运行(第一个参数不是缓冲区)。“get”动词表示它获取与给定过滤器参数匹配的任何内容。“list”动词是不必要的,因为 nvim_get_keymap('')
(空过滤器)返回所有项目。
示例:nvim_buf_del_mark
作用于 Buffer
对象(第一个参数)并使用“del” {verb}
。
更喜欢为给定主题添加一个单独的 nvim_{topic}_{verb}_…
接口。
示例
nvim_ns_add(
ns_id: int,
filter: {
handle: integer (buf/win/tabpage id)
scope: "global" | "win" | "buf" | "tabpage"
}
): { ok: boolean }
nvim_ns_get(
ns_id: int,
filter: {
handle: integer (buf/win/tabpage id)
scope: "global" | "win" | "buf" | "tabpage"
}
): { ids: int[] }
nvim_ns_del(
ns_id: int,
filter: {
handle: integer (buf/win/tabpage id)
scope: "global" | "win" | "buf" | "tabpage"
}
): { ok: boolean }
反例
为同一个
xx
主题创建独立的
nvim_xx
,
nvim_buf_xx
,
nvim_win_xx
和
nvim_tabpage_xx
函数,需要 4 倍的文档量、测试量、样板和接口,用户必须理解,维护者必须维护,等等。因此,以下建议不要使用(将这些 12 个(!) 函数与上述 3 个函数进行比较)
nvim_add_ns(…)
nvim_buf_add_ns(…)
nvim_win_add_ns(…)
nvim_tabpage_add_ns(…)
nvim_del_ns(…)
nvim_buf_del_ns(…)
nvim_win_del_ns(…)
nvim_tabpage_del_ns(…)
nvim_get_ns(…)
nvim_buf_get_ns(…)
nvim_win_get_ns(…)
nvim_tabpage_get_ns(…)
api-clientAPI 客户端包装 Nvim
API 以为各自的平台提供惯用的“SDK”(参见
术语)。你可以为你喜欢的平台或编程语言构建一个新的 API 客户端。
API 客户端包不应以“neovim”或“python-client”之类的含糊不清的名称命名。使用“nvim”作为某个其他标识符的前缀/后缀,以遵循生态系统约定。
例如,Python 包往往在名称中包含“py”,因此“pynvim”是一个不错的名称:它既惯用又明确。如果包名为“neovim”,则会混淆用户,并使文档和讨论变得复杂。
API 客户端包名称示例
✅ OK:nvim-racket
✅ OK:pynvim
❌ NO:python-client
❌ NO:neovim_
使用至少实现 MessagePack 规范第 5 版的 MessagePack 库,该库支持 Nvim 使用的 BIN 和 EXT 类型。
使用单线程事件循环库/模式。
对于用于实现客户端的语言,使用纤维/协程库。这些极大地简化了并发,并允许库在非阻塞事件循环之上公开一个阻塞 API,而无需预先抢占式多任务处理带来的复杂性。
不要假设对 RPC 请求的响应顺序。
客户端应期望请求,这些请求必须立即处理,因为 Nvim 在等待客户端响应时处于阻塞状态。
客户端应期望通知,但这些通知可以“尽快”(而不是立即)处理,因为它们不会阻塞 Nvim。
外部 UI 应该了解
api-contract。 特别是,未来版本的 Nvim 可能会在现有事件中添加新项。 API 向后兼容性强,但客户端在现有事件中添加新(可选)字段时不应中断。
预计外部 UI 将实现这些通用功能
光标样式(形状、颜色)应符合
'guicursor' 属性,该属性由 mode_info_set UI 事件传递。
将 ALT/META(macOS 上的“Option”)键发送为
<M- 键序。
将“超级”键(Windows 键,Apple 键)发送为
<D- 键序。
避免与 Nvim 键映射空间冲突的映射; GUI 有许多新的键序(<C-,>
<C-Enter>
<C-S-x>
<D-x>
)和模式(“shift shift”),这些键序和模式不会与 Nvim 默认设置、插件等发生潜在的冲突。