Snails超级快的模糊搜索框架

Anything, Helm, Ivy

十几年前和rubikitchThierry Volpiatto一起创建了Anything这个模糊搜索框架。

Anything做为当时第一个模糊搜索框架,因为其多后端搜索和强大的框架设计,比当时 ido 的效率要快很多,特别是当你要模糊搜索多个后端,却不想记一堆快捷键的时候,Anything真的很爽。

后来我和rubikitch都忙于工作,Thierry Volpiatto十几年坚持不懈的做了很多代码改进和重构,以至于后来创建了更为健壮和模块化的Helm, Helm到目前为止都是Emacs下最强大的模糊搜索框架。

最近几年,超级黑客Oleh Krehel创建的Ivy, 以其简洁的交互和超快的速度,俘获了大批Emacser的青睐。

Snails一个超快的、现代化和易于扩展的模糊搜索框架

我曾经也尝试过Ivy, 非常简洁,而且速度超快,一点都不卡,但是Ivy有一个和我习惯冲突的设计是,Ivy一次只能搜索一个后端,对于我这种常年用Anything/Helm的老年人来说,让我按照分类来记住不同的搜索后端快捷键,真的很麻烦。

同时,Helm发展了十几年的时间,虽然非常成熟和强大,但是想要基于Helm写一个新的插件,都要看半天文档和小心各种细节。

所以,我有时候在想,我能否重新设计一个新的模糊搜索框架,兼具Helm和Ivy的优点呢?

  1. 像Helm那样支持多个后端同时搜索,特别是最近文件 + 已经打开的文件 + 本地磁盘全局搜索三个后端配合一起搜索的时候,几乎不存在找不到的文件
  2. 像Ivy那样快,不管怎么搜索都是秒开,而且搜索的过程中不要让Emacs变得卡顿
  3. 界面像 Sublime/VSCode 那样,弹出一个单独的固定窗口,不要像Helm那样破坏窗口布局,也不要像Ivy那样在minibuffer上随着输入来回跳动
  4. 交互易用,就是输入搜索字符,上下移动后,按回车就行了,没有其他复杂操作
  5. 插件门槛的编写达到最低,只要会基本的Elisp语法,会写过滤函数,任何插件都应该在5分钟之内完成

所以我创建了Snails这个全新的模糊搜索插件来满足我上面的全部要求, 发一张效果图。

安装

因为这个插件还在活跃开发中,安装和使用文档请见 Github

插件介绍

编写Snails的插件真的超级简单,只要你会基本的Elisp语法,会编写列表过滤函数,你可以在5分钟之内编写完一个新的插件!

Snails从分类上有两种插件语法:同步插件和异步插件

  • 同步插件适用于那些马上可以获取补全列表信息的,比如获取打开的文件,最近浏览的文件这种
  • 异步插件适用于那些需要耗时搜索的操作,比如grep目录,在整个磁盘找文件这种

编写同步插件

我们用内置的snails-backend-recentf插件来讲解怎么编写同步插件:

(require 'snails-core)
(require 'recentf)

(recentf-mode 1)

(snails-create-sync-backend
 :name
 "RECENTF"

 :candidate-filter
 (lambda (input)
   (let (candidates)
     (dolist (file recentf-list)
       (when (or
              (string-equal input "")
              (string-match-p (regexp-quote input) file))
         (add-to-list 'candidates
                      (list
                       (snails-wrap-file-icon file)
                       file)
                      t)))
     candidates))

 :candiate-do
 (lambda (candidate)
   (find-file candidate)))

(provide 'snails-backend-recentf)
  • :name 参数是插件的名字,请为你的插件取一个不同的名字,Snails根据插件的名字来插入搜索结果

  • :candidate-filter 是一个过滤函数, input 参数指用户输入的字符串, 你需要返回一个候选词列表,列表的每一项是一个格式为 (list display-name candidate-content)的对象, 对象的第一个元素 display-name 是显示给用户的候选名字, 对象的第二个元素candidate-content 是真实的补全结果,后面会传给回调函数 candidate-do. 如果什么没有搜索到,可以返回nil,Snails框架会在结果中隐藏返回nil的后端。

  • :candidate-do 是一个确认函数,当你按 Ctrl+m 的时候执行这个函数,函数内容你可以自由发挥。

以上面这段插件代码,它的意思是,如果用户什么都没有输入,显示所有最近浏览的文件,如果有输入,根据用户输入来过滤最近浏览的文件,用户确认后,用 find-file 这个命令打开文件。

编写异步插件

我们用内置的snails-backend-mdfind插件来讲解怎么编写异步插件:

(require 'snails-core)

(snails-create-async-backend
 :name
 "MDFIND"

 :build-command
 (lambda (input)
   (when (and (featurep 'cocoa)
              (> (length input) 5))
     (list "mdfind" (format "'%s'" input))))

 :candidate-filter
 (lambda (candidate-list)
   (let (candidates)
     (dolist (candidate candidate-list)
       (add-to-list 'candidates
                    (list
                     (snails-wrap-file-icon candidate)
                     candidate)
                    t))
     candidates))

 :candiate-do
 (lambda (candidate)
   (find-file candidate)))

(provide 'snails-backend-mdfind)
  • :name 参数是插件的名字,请为你的插件取一个不同的名字,Snails根据插件的名字来插入搜索结果

  • :build-command 是一个命令行参数构建函数,input 是用户输入的字符串, 你需要返回一个字符串列表,列表第一个是调用的Shell工具的名字,列表剩下的字符串是传递给Shell工具的参数。如果你不想进一步搜索,可以直接返回nil。Snails会在结果中隐藏返回nil的后端。

  • :candidate-filter 是一个过滤函数, candidate-list 是Shell命令返回的字符串列表, 你需要返回一个候选词列表,列表的每一项是一个格式为 (list display-name candidate-content)的对象, 对象的第一个元素 display-name 是显示给用户的候选名字, 对象的第二个元素candidate-content 是真实的补全结果,后面会传给回调函数 candidate-do. 如果什么没有搜索到,可以返回nil,Snails框架会在结果中隐藏返回nil的后端。

  • :candidate-do 是一个确认函数,当你按 Ctrl+m 的时候执行这个函数,函数内容你可以自由发挥。

以上面这段插件代码举例,如果用户输入 awesome, 他会构建一个命令字符串列表 (list "mdfind" "'awesome'") 用于创建搜索子进程,当子进程完成时,进程返回候选列表给candidate-fitler过滤函数, 过滤函数过滤Shell命令返回的结果进行候选词渲染。用户确认后,用 find-file 这个命令打开文件。

Snails非常智能,它会管理所有异步后端的子进程,当用户修改输入时,Snails会自动创建新的子进程继续搜索,同时把那些旧的子进程杀掉。不管用户输入多快,都不会卡主Emacs。

最后

Snails插件编写简单吧?

如果你和我有同样的口味,欢迎试用。

如果你想玩一下Elisp,欢迎编写新的插件。:wink:

33 个赞

Snails 可以看做是 Anything、Helm、Ivy同类产品。

Snails 的目标就是快、多后端搜索、插件编写容易。

欢迎各位大佬贡献补丁。

1 个赞

喜闻乐见,ivy 和 helm有了新的竞争者 :+1:

以这些特性来说,very promising,我感觉现在要做的是吸引更多用户/开发者。虽然懒猫嫌 melpa 麻烦,但是还是建议 release 一下,是吸引用户/开发者的一个挺好的途径。

希望可以支持helm-mini那样的魔术字符搜索“*rust搜索rust-mode的buffer,@contains搜索buffer内含有contains的buffer”我写fuz.el也是希望可以达到fzf,sk这些插件的分词fuzzy search的效果。

大佬欢迎贡献补丁

大佬这名字消受不起啊 :joy:。我先试用一下


的确不错!虽然现在简陋了点,不过给我眼前一亮的感觉

希望内置你的fuz.el进去,现在搜索算法做的还很基础

提了个ESC ESC ESC退出的PR

1 个赞

牛逼牛逼,已经合并了,谢谢

我是佛性开发者,喜欢就用,不做过多宣传。

宣传之后,就会被无止境的 feature request, issues 干扰,我觉得懒猫现在状态挺好的。自己用着开心就好,想尝试的Lisp黑客自然会去读代码,去提PR。

PS. 今天折腾了几个小时,就写了几个 magit 函数,不经常写 elisp,效率好低啊 :joy:

1 个赞

对的对的,我写插件主要是给自己用,别人喜欢就行,不喜欢的我也不想理他们的需求。

我剩下的人生就是为自己而活,自己开心和陪家人最重要。

8 个赞

但是 PR 也会多起来啊,feature request 和 issues 不看就好了(这样想用的人又无法忍受 issue 的就只能自己提 PR 解决了 :joy:)

1 个赞

很高兴看到 snails 的出现。新的竞争者不是坏事,可以互相推动发展。后来者总是可以先总结前任的优缺点,开发出更好的框架。希望 snails 能尽早稳定 core,吸引更多的开发者贡献 plugins,就像 ivy 和counsel一样。helm 实在是太过于笨重、复杂。

我也再次建议,snails 可以放到 mepla,如果时间允许很乐于能参与其中。当然,前提是snails 要先得到大家的认可。

Update:@manateelazycat,刚试用了下,效果与 ivy-posrame 有点类似。但是发现一个问题,如果 frame 被 split 成几个 window,运行 snails 会直接弹出类似的 frame。

2 个赞

使用了下,可能是我的字体的原因吧,搜索框盖住了一部分字体,只能显示字体的下半身。。

frame 被 split 成几个窗口估计跟你的设置有关系。

我一会加一个补丁,不管你的配置怎样都保证 frame 是一个窗口。

已经加到todolist了,会根据字体大小来动态计算输入框的高度

花了5分钟写了一个基于 fd 命令来搜索当前项目文件的后端:

总共就30多行代码

这个挺不错,相当轻量级,action只有一个吗?另外能支持多选吗?比如选择两个文件进行diff

不建议那样做,那样做的话用helm吧。 这个框架的目的就是保持快速简单