本文将对Neovim热门搜索插件 Telecope.nvim 进行魔改,虽然改动的代码逻辑不多,但可以在前端npm项目中提高文件搜索的舒适度。同时如果读者想自己对代码进行魔改本文也会将我在修改Telescope.nvim 代码过程中对我理解其架构有较大帮助的文章贴在本文最后。
一、动机
我主力语言是JavaScript,而项目通常使用npm进行搭建,因此每一个项目中都有一个node_modules文件夹存放依赖,而Telescope.nvim配置中只能将文件夹直接排除在搜索结果外,而某些时候需要直接对node_modules文件夹内依赖文件进行搜索的目的就无法达到,因此我在阅读了Telescope.nvim部分源码后将其中的关于文件夹排除的逻辑修改了下,用户可以通过配置文件提供对于文件夹的别名,而通过at符号加文件夹缩写名对被排除文件夹内的文件进行搜索。
在Telescope.nvim原生配置中只能通过在配置文件中将需要排除的文件夹填入defaults.file_ignore_patterns表中,而且一旦将文件夹填入该表,则任何情况下都无法搜索该文件夹下的内容。

二、目标
通过修改Telescope.nvim源码为file finder添加filter功能,filter功能定义如下:
- 用户可通过配置文件为不同文件夹创建缩略名
- 用户可通过配置文件将文件夹及内容排除在普通搜索结果外
- 用户可在搜索框内输入prefix符号+文件夹缩略名+空格+文件模糊名对缩略名对应文件夹内容进行搜索
可以看到其实功能不是很复杂,但需要先对Telescope.nvim的架构进行一定了解。
Telescope.nvim架构
本节中对于Telescope架构内容的描述及介绍来源于其源码及其DeveloperDoc。
首先Telescope被分为了Layout、Picker、Finder、Sorter几个主要部分,下面我们分别对这四个部分进行简单的介绍:
Layout
Layout是Telescope.nvim中直接与用户进行交互的UI部分,在配置文件中可以通过修改defaults.layout_strategy及defaults.layout_config进行修改,这部分和我们需要修改的代码没有关联。
Picker
Picker是与用户进行交互的UI部分背后的逻辑,直译这个单词”拾取器“也可以比较明确表明这个组件是通过将用户的输入转化为一种过滤策略进而拾取用户可能需要的部分数据并展现出来,而我们可以从源码上找到对于file_ignore_patterns的实现也是在Picker中。因此Picker是我们进行改造的重点。

Finder
Finder是位于Picker下层的组件,搜索的数据通过finder进行获取,而finder中则是通过对Picker指定的命令进行执行从而获取数据,而以文件为例,在我的电脑上安装了Telescope推荐的fd作为依赖,因此这里将会执行fd搜索命令。


Sorter
Sorter用于对搜索结果的排序,在获取到搜索结果数据后,数据将会被送往Sorter进行排序。

而Telescope推荐使用native sorter例如fzf或者fzy提高性能:

三、修改步骤
有上面对于Telescope架构基础了解之后,我们就可以开始思考从哪里入手对于Telescope的改造,实际上已经比较明确了,因为file_ignore_patterns的代码逻辑所在的地方实际就是我们需要进行修改的地方。
Step1.设计配置文件格式
首先我们需要设计配置文件的格式,这里我对于这部分配置文件的设计如下:
defaults={
-- general为约定的普通搜索,即不使用@开始的过滤器进行搜索的方法
filter={
-- @m react
m={ -- 文件夹缩写名
map="node_modules", -- 文件夹实际名
excludeFrom={"general"}, -- 将该文件夹从普通搜索中排除
},
-- @d config.md
d={
map="doc",
excludeFrom={"general"}
}
},
.....
}
这里我们约定使用general代表普通搜索,而若要对从general搜索中排除的文件夹内容进行搜索需要使用at符号加文件夹缩写+空格+文件模糊名进行搜索。
Step2.添加配置文件校验
因为Telescope默认会将未知配置项过滤掉,所以首先我们需要在Telescope配置文件校验处添加我们的配置文件入口。而配置文件校验在telescope/cofngi.lua进行添加,我们在文件末尾添加以下代码:
append(
"filter",
nil,
[[
Customizable Filter
]]
)
这里通过Telescope的helper函数append将filter配置块添加进了telescope_defaults表中,而在telescope初始化加载配置文件时该表中配置项将作为配置有效性参考将未知配置块过滤。
Step3.解析filter配置块
由于设置了general为普通搜索下的过滤配置,为了方便后续编程,需要将general下的配置从filter配置块中提取出来,而这里暂时只需要记录general下需要排除的文件夹,下面是general配置块的结构:
filter={
general={
exclude={
"node_modules",
"doc"
}
}
}
因此我们首先需要获取到filter配置块,而后在配置块中添加general及general.exclude,最后将具体需要exclude的文件夹添加到其中:
local filter = config.values.filter
local general ={}
general.exclude = {}
local function has_value (tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
for key, value in pairs(filter) do
-- print(key .. vim.inspect(value))
if key ~= "general" and has_value(value.excludeFrom,"general") then
general.exclude[key] = value.map
end
end
filter.general = general
上述代码首先将filter配置块从config中取出,而后创建general及general.exclude,并创建一个帮助函数has_value。而下方的for循环则是遍历filter配置块并查找excludeFrom配置中是否包含general,若包含则将该文件夹添加到general.exclude中。
而后需要将该配置添加到picker对象的metatable中,这里就直接在下方的setmetatable函数入参中添加一行就可以了:
local obj = setmetatable({
......
file_ignore_patterns = get_default(opts.file_ignore_patterns, config.values.file_ignore_patterns),
filter = config.values.filter or {},
......
})
Step4.添加filter逻辑代码
经过阅读源码我们发现,file_ignore_patterns实际起作用的地方在Picker:get_result_processor函数中,而这个函数将会在每次打开Telescope搜索框是出现,因此我们对filter的逻辑代码将会添加在这个函数中。

log.trace("Processing result... ", entry)
-- Filter Here
-- Get @m or someting going to filter
if string.sub(prompt,1,1) == "@" then
-- 获取缩略文件名
local prefix = string.sub(prompt,2,2)
-- print('filter:@' .. prefix)
-- 获取filter配置
local filter = rawget(self.filter,prefix)
if filter then
-- print(vim.inspect(filter) .. "map" .. filter.map)
-- 排除非目标文件夹文件
local file = vim.F.if_nil(entry.filename, type(entry.value) == "string" and entry.value) -- false.if none is true
-- print(filter.map .. "-in?->" .. file .. '='.. (string.find(file,filter.map) or ""))
if file then
if string.find(file, filter.map) == nil then
self:_decrement "processed"
return
end
end
end
-- 使用Sorter进行排序
self.sorter:score(string.sub(prompt,4), entry, cb_add, cb_filter)
else
-- 普通搜索
local general = rawget(self.filter,"general")
if general then
local file = vim.F.if_nil(entry.filename, type(entry.value) == "string" and entry.value) -- false if none is true
if file then
-- 检查该文件是否存在于exclude数组文件夹中
for key, value in pairs(general.exclude) do
-- print(value .. "-in?->" .. file .. '='.. (string.find(file,value) or ""))
if string.find(file,value) then
-- print('in')
self:_decrement "processed"
return
end
end
end
end
-- 使用Sorter进行排序
self.sorter:score(prompt, entry, cb_add, cb_filter)
end
-- for _, v in ipairs(self.file_ignore_patterns or {}) do
-- local file = vim.F.if_nil(entry.filename, type(entry.value) == "string" and entry.value) -- false if none is true
-- if file then
-- if string.find(file, v) then
-- log.trace("SKIPPING", entry.value, "because", v)
-- self:_decrement "processed"
-- return
-- end
-- end
这里我直接将file_ignore_patterns的实现逻辑给注释了,然后在上面添加了我自己的filter实现代码,代码不是很难,就是一个字符串匹配逻辑,这里我没有看self:_decrement “processed”函数的内部逻辑,但向来是相当于将该行搜索结果进行了过滤,而时间上也不允许我再继续深入了,而经过测试这部分代码也运行正常(后面有时间再继续研究Telescope,感觉用middleware实现这类搜索器拓展性更好一点)。
代码变化
可以看到只对config.lua 和pickers.lua进行了更改。

四、效果
从效果上来讲还是不错的,至少对于前端npm项目来说还是非常有用的:
普通搜索下因为对node_modules进行了排除,因此不会显示node_modules下的文件。

而使用at符号加m进行搜索时,则会只显示node_modules文件夹下的文件:

五、总结
花了一下午对Telescope的源码进行阅读并进行修改,感觉收益是不错的,同时对lua和这类程序的架构和设计有了进一步了解,因此我始终认为在阅读文档后实践使用并搭配阅读源码是对程序员提高最快的路径。
v1.0 wep 完成基本的filter功能