Skip to content

5. 插件开发(plugin develop)

lijing00333 edited this page Aug 21, 2025 · 1 revision

软件架构(Architecture)

VimL 虽然已经支持异步,但它的异步很弱,容易卡死,所以在触发 complete() 的时机上,选择使用 vim 自带的事件队列,插件的注册都是基于事件来完成,而不是直接函数调用。

为了尽可能保持简单,插件没有基于 RPC,高计算量的部分尽量用 lua 来做,在兼容性基础上也保障了基本性能上的体验。

为了避免过于频繁的lsp调用,匹配逻辑做了区分。分为首次匹配(TextChangedI),和菜单匹配(TextChangedP),lsp 请求只出现在首次匹配。

源的调用顺序为:path(同步)→lsp(异步)→Tabnine(异步)→snip(同步)→buf(同步)→dict(同步)。

插件结构(Vimscript)

所有内置插件都在 sources 目录下。 插件包含几个关键方法(以 go 为例):

  • easycomplete#sources#go#constructor:插件初始化,这里主要是注册 lsp server。
  • easycomplete#sources#go#completor:补全主方法
  • easycomplete#sources#go#GotoDefinition:跳转到定义
  • easycomplete#sources#go#filter:对补全的返回结果做一层过滤(可留空)

一个插件只需要最少实现前三个方法即可。前三个方法重点是第一个 constructor 方法,用作初始化 lsp server,补全方法和跳转到定义通常情况下不同语言不会有差异,直接调用系统默认的方法即可,但如果 lsp 的实现不够标准,则需要重写这两个方法。

实现了插件主体之后,需要将插件注册,注册方法是监听easycomplete_default_plugin事件:

au User easycomplete_default_plugin call easycomplete#RegisterSource({
      \ 'name': 'go',
      \ 'whitelist': easycomplete#FileTypes("go", ["go"]),
      \ 'completor': 'easycomplete#sources#go#completor',
      \ 'constructor' :'easycomplete#sources#go#constructor',
      \ 'gotodefinition': 'easycomplete#sources#go#GotoDefinition',
      \ "root_uri_patterns": [
      \    "go.mod",
      \ ],
      \ 'command': 'gopls'
      \ })

最后是 lsp 服务的安装脚本,安装脚本在 installer 目录下,保存为和插件同名的脚本。

当然也可以不提供 lsp 的安装脚本,可以通过 mason.nvim 来安装管理 lsp 服务。

插件结构(lua)

同样,lua 形式的插件也需要同样的方式注册(以 easycomplete-docker 为例):

文件结构:

.
├── after
│   └── plugin
│       └── init.lua
└─── lua
    └── easycomplete_docker.lua

注册插件:

-- lua/easycomplete_docker.lua
easycomplete.register_source({
      name           = "docker",
      whitelist      = {"docker", "dockerfile"},
      completor      = require('easycomplete_docker').completor,
      constructor    = require('easycomplete_docker').constructor,
      gotodefinition = require('easycomplete_docker').goto_definition,
      filter         = require('easycomplete_docker').filter,
      command        = "docker-langserver"
    })

插件注册完成后同样需要定义四个函数,可以是任意位置,我这里就放一起了:

  • require('easycomplete_docker').completor
  • require('easycomplete_docker').constructor
  • require('easycomplete_docker').goto_definition
  • require('easycomplete_docker').filter

然后在启动脚本中绑定easycomplete_default_plugin事件来完成插件的注册。

-- after/plugin/init.lua
vim.api.nvim_create_autocmd("User", {
  pattern = "easycomplete_default_plugin",
  callback = function()
    require("easycomplete_docker").setup()
  end,
})

lsp 服务 docker-langserver 通过 mason 来安装即可。

常用 API(Lua)

require("easycomplete")

local esy_cmp = require("easycomplete")

插件生命周期相关的方法

esy_cmp.config(opt)

配置函数,配置方法参照这里

其中:

require("easycomplete").config({nerd_font = 1})

等价于

vim.g.easycomplete_nerd_font = 1

可以通过 config() 中配置 setup 函数,来写其他的绑定map等逻辑:

local esy_cmp = require("easycomplete")
esy_cmp.config({
  nerd_font = 1,
  setup = function()
    -- 初始化的代码
  end
})

esy_cmp.setup(func)

初始化代码,可以跟 config() 分开来使用,这段代码和上面的例子功能一样:

local esy_cmp = require("easycomplete")
esy_cmp.setup(function()
  vim.g.easycomplete_nerd_font = 1
  -- 其他初始化代码
end)

esy_cmp.fuzzy_search(needle, haystack)

fuzzy 比对,比对成功返回 true,比对失败返回 false。

esy_cmp.matchfuzzy_and_filter(match_list, needle)

vim.fn.matchfuzzy 的重新实现,只返回结果,不返回分数

esy_cmp.register_source(tb)

注册插件,参照上文“插件结构”

esy_cmp.register_lsp_server(opt, tb)

注册lsp server,参照上文“插件结构”

esy_cmp.get_command(plugin_name)

根据 传入的 plugin_name 返回 lsp server 的命令。

esy_cmp.get_default_root_uri()

获得vim-easycomplete插件的根目录。

esy_cmp.do_lsp_complete(opt, ctx)

执行lsp的complete查询,返回 nil。

esy_cmp.do_lsp_defination()

执行goto跳转到定义处,返回 nil

require("easycomplete.util")

常用的工具函数

util.get_item_plugin_name(item)

参数是一个匹配选项实例,返回这个匹配项来自哪个源,返回源的名字 plugin_name,比如返回 "vim"、"py"、"go" 等

util.get_typing_word()

获得当前光标处正在敲入的单词

util.get_buf_keywords(typing_word)

根据当前正在输入的单词,获得当前 缓冲区中的匹配的单词列表

util.check_noice()

检查 noice.nvim 是否安装并启用了,返回 true 或者 false

util.trim_before(str)

把一个字符串的前缀空格去掉,返回去掉前缀空格的字符串。

util.console(arg1, arg2, arg3, ...)

打印日志,返回值 nil,日志位置在~/.config/vim-easycomplete/debuglog,通过tail -f ~/.config/vim-easycomplete/debuglog 来查看日志输出

util.parse_abbr(str)

把超长的匹配词结果根据长度限制进行截断,返回截断的字符串,比如 abcdefghijklmnabcdefghij...。长度限制取自vim.g.easycomplete_pum_maxlength

util.zizz()util.zizzing()

zizz() 执行一个 30ms 的定时器,返回 nil,zizzing()检查这个定时器还在的话就返回 true,否则返回 false

util.filter(t, func)

类似 vim.fn.filter 的过滤函数,t 为待过滤的表,func 是判定函数,返回过滤完成后的表。

util.distinct(items)

表的去重,items 是字符串组成的表{"abc","def","ghi",...},返回去重后的表。

util.complete_menu_filter(matching_res, word)

用 word 过滤补全的表 matching_res,返回过滤好的表,表元素同时完成了 fuzzy 匹配结果的位置标记。

util.isTN(item)

判断给定的 item 是否是 tn 类型,是 返回 true,否 返回 false。

util.current_plugin_ctx()

获得当前 lsp 所在插件的上下文。

util.current_lsp_name()

获得当前 lsp 服务的名称

util.get_default_lsp_root_path()

获得当前 lsp 插件安装的根目录。

util.current_plugin_name()

获得当前文件所使用的 lsp 源插件的名字。

require("easycomplete.pum")

补全窗口(菜单)相关的函数

pum.complete(start_col, menu_items)

补全函数,参数和功能同 vim 原生 complete() 函数

pum.close()

关闭补全窗口

pum.bufid()

获得补全窗口关联的 buffer id

pum.winid()

获得补全窗口的窗口id

pum.visible()

判断补全窗口是否可见

pum.selected()

判断补全窗口是否有选中项,返回 true 或者 false

pum.selected_item()

获得当前选中的补全项

pum.select(index)

选中补全窗口中的某一项,index 从 1 开始计数

pum.select_next()

补全窗口选中下一个

pum.select_prev()

补全窗口选中上一个

pum.get_path_cmp_items()

获得当前位置可匹配出的目录和文件列表

LSP Server 安装

自定义插件 lsp 的安装建议用 mason

Clone this wiki locally