telescope.nvim 插件开发指南:从零编写你的第一个 Picker 与 Extension
【免费下载链接】telescope.nvimFind, Filter, Preview, Pick. All lua, all the time.项目地址: https://gitcode.com/GitHub_Trending/te/telescope.nvim
本指南以仓库根目录的 developers.md 为核心,面向希望为 telescope.nvim 编写自定义 Picker(选择器)或扩展(Extension)的开发者,系统讲解 Picker、Finder、Action、Entry Maker、Previewer 五大组件的编写方法与底层机制,并配套展示仓库内源码实现作为依据。读完本文,你将能够独立完成一个可运行的自定义 Picker、替换默认动作、通过 Entry Maker 定制展示与排序,并将其打包成可通过:Telescope命令调用的扩展。
阅读前提:本指南假设你已具备一定的 Lua 编程基础(telescope 面向 Neovim,使用 Lua 5.1 语法)。如果你刚开始接触 Lua,建议先学习 Lua 5.1 手册 以及 Neovim 下的 Lua 用法。对 telescope 的整体架构(Picker / Finder / Sorter / Previewer 之间的数据流)还不太熟悉的话,可以先通过
:h telescope.nvim查看架构流程图。
编写你的第一个 Picker
1. 准备一个 Lua 草稿文件
建议打开一个空的 Lua 草稿文件,在其中逐步开发 Picker,并通过:luafile %反复执行验证。到文章末尾,我们会把这个文件打包成正式的扩展。
2. 必需的 Requires
开始编写 Picker 前,先在文件顶部引入三个核心模块:
local pickers = require "telescope.pickers" local finders = require "telescope.finders" local conf = require("telescope.config").values| 模块 | 作用 |
|---|---|
pickers | 主模块,用于创建新的 Picker 实例 |
finders | 提供各种 Finder 接口,用来向 Picker 填充条目(items) |
config | config.values表保存了用户的配置,直接将其引用到conf,可让 Picker 尊重用户的 sorter、theme 等自定义设置 |
conf是全文反复用到的关键:把它传给 sorter、previewer 等组件,就能让自定义 Picker 自动继承用户在setup()里配置的行为(例如用户若配置了 fzf-native sorter,Picker 会自动采用它)。
3. 第一个 Picker:一个最简单的颜色选择器
-- our picker function: colors local colors = function(opts) opts = opts or {} pickers.new(opts, { prompt_title = "colors", finder = finders.new_table { results = { "red", "green", "blue" } }, sorter = conf.generic_sorter(opts), }):find() end -- to execute the function colors()执行:luafile %后,会打开一个包含red、green、blue三个条目的 telescope Picker。但此时按回车选择一个颜色会打开一个新文件——这正是我们下一步要解决的问题。
逐段拆解这段代码:
colors函数接收一个opts表。这是良好的实践:用户可以通过传入自己的opts表来改变 Picker 的行为。例如在opts中传入主题配置即可更换 Picker 的显示主题,这正是把opts作为第一个参数传给pickers.new的原因。prompt_title是可选字段,未设置时默认显示Prompt。finder是必填字段,必须赋值为某个 finders 函数的返回值。这里的new_table允许定义一组静态结果,results是元素数组——它不一定是字符串数组,也可以是表数组(下一节会用到)。sorter虽不是必填,但强烈建议设置:默认值是empty(),意味着没有附加任何 sorter,结果无法过滤。实践上建议设为conf.generic_sorter(opts)或conf.file_sorter(opts)。从conf取值会自动尊重用户配置(例如用户启用了 fzf-native,就会自动挂载对应 sorter);同时把opts也传进去是因为 sorter 可能用到它,比如 fzf sorter 可以通过opts切换大小写敏感/不敏感。- 定义完 Picker 后必须调用
find()才能真正启动它。
4. 通过 opts 切换主题
得益于opts的透传,我们可以用dropdown主题来打开 Picker(把上一节的调用行替换为):
colors(require("telescope.themes").get_dropdown{})仓库中 lua/telescope/themes.lua 定义了get_dropdown、get_ivy等主题工厂函数,它们的返回值就是一份可供pickers.new消费的 opts 表。
替换默认 Action
现在解决"选中颜色却打开了新文件"的问题。这需要替换默认的 select 动作。之所以选择"替换"而非"把新函数映射到<CR>",是因为替换会尊重用户的配置:如果用户已把select_default重映射到其他按键,替换后的逻辑依然会按用户习惯触发。
为此,需要在文件顶部追加两个 requires:
local actions = require "telescope.actions" local action_state = require "telescope.actions.state"actions:保存了所有可被用户映射的动作,我们需要它来访问默认动作并替换它(参见:help telescope.actions)。action_state:提供若干工具函数,用于获取当前 Picker、当前选中项、当前输入行(参见:help telescope.actions.state)。
然后在我们传给pickers.new的表(例如sorter之后)中新增attach_mappings键:
attach_mappings = function(prompt_bufnr, map) actions.select_default:replace(function() actions.close(prompt_bufnr) local selection = action_state.get_selected_entry() -- print(vim.inspect(selection)) vim.api.nvim_put({ selection[1] }, "", false, true) end) return true end,关键点说明:
attach_mappings的值是一个函数,它必须返回true或false。返回false表示只挂载该函数内定义的动作,这会移除默认的move_selection_{next,previous}等移动选择的动作,因此绝大多数情况下应当返回true。若函数没有任何返回值,会抛出错误。- 该函数有两个参数:
prompt_bufnr是 prompt 缓冲区的编号,可据此拿到 Picker 对象;map是用于把动作或函数映射到任意按键序列的函数。 select_default默认映射到<CR>。替换它需要调用actions.select_default:replace并传入新函数。- 新函数中先调用
actions.close关闭 Picker,再用action_state取回selection。注意:即使 Picker 已经关闭,依然可以用action_state获取选中项和当前输入(action_state.get_current_line())。 - 用
print(vim.inspect(selection))查看会发现 selection 与我们输入的字符串不同——因为 telescope 内部会把它打包成带多个键的表。这个行为可由 Entry Maker 自定义(下一节)。 - 最后对 selection 做点实事:本例用
vim.api.nvim_put把文本放入当前缓冲区。
从源码看,actions.select_default:replace的底层实现在 lua/telescope/actions/mt.lua:action 实际是一个带元表(metatable)的对象,replace会调用replace_map { [true] = v }把新函数写入_replacements表;执行时run_replace_or_original会先检查所有 replacement 条件,没有匹配才回退到原始函数(original_func(...)),这就保证了"替换"而非"覆盖"的语义。同一个文件中还提供了replace_if(condition, replacement)(仅当condition返回 true 时替换)与replace_map(tbl)(以函数为键的条件映射表)等更细粒度的手段,我们将在"技术解析"部分详述。
Entry Maker:定制展示与匹配
Entry Maker 是一个把 Finder 返回的原始条目转换为内部 entry 表的函数,entry 表有几个必需的键。它的价值在于:展示的字符串与参与匹配/排序的字符串可以完全不同;处理文件时还能同时设置绝对路径(保证文件总能被找到)与用于展示和排序的相对路径(这个相对路径甚至不需要在当前工作目录下有效)。
现在为颜色示例定义 entry_maker,并把 results 改成更有内容的表数组:
finder = finders.new_table { results = { { "red", "#ff0000" }, { "green", "#00ff00" }, { "blue", "#0000ff" }, }, entry_maker = function(entry) return { value = entry, display = entry[1], ordinal = entry[1], } end },新的 results 是表数组,每个表包含颜色名与十六进制色值。entry_maker依次接收每个表并产出 entry:
value:推荐保存对原始条目的引用,这样在 action 里总能拿到完整的原始表。display:必填,可以是字符串,也可以是function(tbl)(tbl是 entry_maker 返回的表,因此可以访问到value、ordinal等字段)。如果条目很多,建议用函数形式的display(尤其当你要修改展示文本时),这样它只会在条目实际被显示时执行,避免不必要的开销。ordinal:必填,用于排序/过滤。正因为 display 与 ordinal 分离,我们才能让 display 携带图标、特殊标记等复杂内容,而 ordinal 只保留简单的排序键。
除以上键外,还有几个在本例中用不到但处理文件时很重要的键:
path:设置文件的绝对路径,保证文件始终能被找到;lnum:指定文件中的行号,让conf.grep_previewer能定位到该行,并让默认动作跳转到该行。
仓库中 lua/telescope/make_entry.lua 是内置 Entry Maker 的权威示例,其文件头部注释还列出了 entry 表的完整键位,包括可选的valid(设为 false 时 Picker 不展示该条目)、filename(默认<CR>动作会将其解释为打开该文件)、bufnr、col等。想让 display 呈现类似表格的多列效果,可以借助 lua/telescope/pickers/entry_display.lua 中的 displayer;一个更简单的 displayer 示例是 make_entry.lua 中的gen_from_git_commits函数,它用entry_display.create构造了"8 列哈希 + 剩余宽度提交信息"的展示布局,并返回一个闭包作为 entry_maker。
Previewer:何时需要
本示例(基础颜色选择器)不需要 Previewer,这是更进阶的主题,在:help telescope.previewers中有完善说明。如果你需要一个不带列的文件预览器,默认应选用conf.file_previewer或conf.grep_previewer——和 sorter 同理,从conf取值会自动尊重用户配置。
Oneshot Job:异步外部进程结果
oneshot_jobFinder 用于启动一个异步外部进程,进程逐行输出结果,每行都会调用entry_maker生成条目。典型用法是把find命令的结果喂给 Picker:
finder = finders.new_oneshot_job({ "find" }, opts ),源码实现见 lua/telescope/finders.lua 的finders.new_oneshot_job:它把命令列表的第一个元素当作command、其余作为args,返回一个async_oneshot_finder(由 lua/telescope/finders/async_oneshot_finder.lua 提供),并支持entry_maker、cwd、maximum_results、split_char等 opts 选项。
更多示例
- lua/telescope/builtin 目录包含全部内置 Picker,是寻找更多编写范式的最佳去处(内置 Picker 也正是用本文介绍的概念写成的)。
- 社区已有很多基于这些概念编写的扩展可供参考(例如 telescope-fzf-native 这类提供替代 sorter 的扩展)。
- 读完本指南仍有疑问,可以到项目 Discussions 提问。
打包为 Extension
要把 Picker 打包成可通过:Telescope命令调用的扩展,需要按如下结构组织插件,保证 telescope 能够发现它:
. └── lua ├── plugin_name # Your actual plugin code │ ├── init.lua │ └── some_file.lua └── telescope └── _extensions # The underscore is significant └─ plugin_name.lua # Init and register your extension注意_extensions目录名的下划线是有意义的。lua/telescope/_extensions/plugin_name.lua文件需要返回如下结构(参见:help telescope.register_extension):
return require("telescope").register_extension { setup = function(ext_config, config) -- access extension config and user config end, exports = { stuff = require("plugin_name").stuff }, }setup函数可以访问扩展配置与用户 telescope 默认配置,用于设置扩展专属的全局配置,也允许覆盖内部函数——例如为扩展提供替代 sorter(像 telescope-fzf-native 那样)。exports表声明导出的 Picker,之后可以通过Telescope plugin_name stuff访问。如果只导出一个功能,建议把键名与插件名保持一致,这样直接用Telescope plugin_name即可调用。
仓库中的扩展注册机制实现在 lua/telescope/_extensions/init.lua:extensions.register原样返回模块;load_extension用pcall(require, "telescope._extensions." .. name)加载模块并给出友好错误;extensions.manager的元表会在首次访问某个扩展时调用其setup(extensions._config[k] or {}, require("telescope.config").values),并把ext.exports暴露给用户——这就是require("telescope").extensions.foo背后发生的事情。文档注释还指出,exports中键名不以_开头且值为函数的条目,会在启用include_extensions选项的内置 Picker 中一并出现。
技术解析
Picker 的可配置字段
以下摘录自 lua/telescope/pickers.lua,是创建自定义 Picker 时的字段总览:
-- lua/telescope/pickers.lua Picker:new{ prompt_title = "", finder = FUNCTION, -- see lua/telescope/finders.lua sorter = FUNCTION, -- see lua/telescope/sorters.lua previewer = FUNCTION, -- see lua/telescope/previewers/previewer.lua selection_strategy = "reset", -- follow, reset, row border = {}, borderchars = {"─", "│", "─", "│", "┌", "┐", "┘", "└"}, default_selection_index = 1, -- Change the index of the initial selection row }其中selection_strategy在源码(pickers.lua)中实际支持row、follow、reset、closest、none等多种策略,用于定义 prompt 内容变化时选中行如何迁移;borderchars与window配置联动,未显式设置时回落到config.values中的默认值。default_selection_index控制 Picker 初始选中第几行(从 1 开始)。
Finder 的字段
摘录自 lua/telescope/finders.lua:
-- lua/telescope/finders.lua Finder:new{ entry_maker = function(line) end, fn_command = function() { command = "", args = { "ls-files" } } end, static = false, maximum_results = false }从实现看,Finder 家族包括:JobFinder(通过外部 Job 获取结果并边到达边处理)、DynamicFinder(调用opts.fn(prompt)同步返回结果列表)、以及new_table背后的async_static_finder、new_oneshot_job背后的async_oneshot_finder与new_job的async_job_finder(分别见 lua/telescope/finders/async_static_finder.lua、async_oneshot_finder.lua、async_job_finder.lua)。maximum_results对实时更新的大型查询尤其有用,可限制处理的结果数量。
覆盖 Actions / Action Set
以下文件是理解 action 机制的关键:
- lua/telescope/actions/init.lua:最"面向用户"的文件,包含我们提供的全部内置动作。
- lua/telescope/actions/set.lua:第二"面向用户"的文件,提供被多个内置动作共同消费的 action set,从而允许只覆盖其中一项,而不必复制多份相同配置/函数。
- lua/telescope/actions/state.lua:提供在 action 内部与 telescope 状态交互的 API(如
get_selected_entry()、get_current_line()、get_current_picker(prompt_bufnr),见 state.lua),对编写自定义 action 很有用。 - lua/telescope/actions/mt.lua:定义了 action 的行为机制,一般情况下无需深入,但了解它能更好地理解下面的替换 API。
:replace(function)—— 直接覆盖
local actions = require('telescope.actions') actions.select_default:replace(git_checkout_function):replace_if(conditional, function)—— 条件覆盖
local action_set = require('telescope.actions.set') action_set.select:replace_if( function() return action_state.get_selected_entry().path:sub(-1) == os_sep end, function(_, type) -- type is { "default", "horizontal", "vertical", "tab" } local path = actions.get_selected_entry().path action_state.get_current_picker(prompt_bufnr):refresh(gen_new_finder(new_cwd), { reset_prompt = true}) end ):replace_map(configuration)—— 多条件映射
local action_set = require('telescope.actions.set') -- Use functions as keys to map to which function to execute when called. action_set.select:replace_map { [function(e) return e > 0 end] = function(e) return (e / 10) end, [function(e) return e == 0 end] = function(e) return (e + 10) end, }结合 mt.lua 的实现可以更清晰地理解这套 API:每个 action 都是一张带元表的表,内部维护_static_pre、_pre、_replacements、_static_post、_post等表;调用 action 时先执行静态前置钩子,再经run_replace_or_original按序尝试各个 replacement 条件(条件为true或条件函数返回真即命中),全部不命中才执行原始函数,随后执行后置钩子。replace等价于replace_map { [true] = v },即无条件替换;replace_if则是单条件版本。此外enhance(opts)可以给 action 附加pre/post钩子,__add元方法还支持把多个 action 组合成一个。
Previewers
参见:help telescope.previewers,仓库实现位于 lua/telescope/previewers/(含buffer_previewer.lua、term_previewer.lua、previewer.lua等),其中previewer.lua是自定义 Previewer 的基类所在。
小结
至此,你已完成从零到一的完整链路:用pickers.new+finders.new_table创建静态 Picker → 用attach_mappings+actions.select_default:replace替换默认动作 → 用entry_maker定制展示/匹配/排序 → 用finders.new_oneshot_job接入异步外部进程 → 最后把整套逻辑打包成_extensions扩展并通过register_extension暴露给:Telescope命令。借助conf与opts的透传,你的自定义 Picker 还能自动继承用户的 sorter、主题与按键配置,真正做到"All lua, all the time"。
【免费下载链接】telescope.nvimFind, Filter, Preview, Pick. All lua, all the time.项目地址: https://gitcode.com/GitHub_Trending/te/telescope.nvim
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考