Home

Awesome

<img src="./images/snails.png" width="200">

This project has been replaced by blink-search, thank you!

What is snails?

Snails is a modern, easy-to-expand fuzzy search framework.

The goal of this project is to minimize the development threshold for fuzzy search plugins. If you know how to write a filter function, you can write a new search plugin in 5 minutes, regardless of how complex the search framework is.

Installation

  1. Clone or download this repository (path of the folder is the <path-to-snails> used below).
  2. If you are using Mac, install exec-path-from-shell dependency.
  3. In your ~/.emacs, add the following two lines:
(add-to-list 'load-path "<path-to-snails>") ; add snails to your load-path
(require 'snails)

Usage

M-x snails to launch snails

You can use M-x snails-search-point to launch snails with symbol around point.

<img src="./images/screenshot.png">

You can customize snails-default-backends and snails-prefix-backends with your own prefix rule.

Use Snails without popup frame.

Default snails popup new frame to search candidates. If you don't like popup style, you can change to helm style with below code:

(setq snails-show-with-frame nil)
<img src="./images/frame_screenshot.png">

Use Snails With Custom Backends

You can also customize the search backends you want to use! (similar to Ivy)

You can either write your own backend (see below) or assemble all available backends like the following:

Just search opened buffers (use snails with 1 backend only!)

(snails '(snails-backend-buffer))

Search opened buffers and recently opened files (use snails with 2 backends!)

(snails '(snails-backend-buffer snails-backend-recentf))

Search symbol at point of opened buffers and recently opened files (use snails with 2 backends!)

(snails '(snails-backend-buffer snails-backend-recentf) t)

Search customize string hello

(snails nil "hello")

When you customize the search backends, snails won't filter search result with input prefix.

Currently Available Backends

BackendDescription
snails-backend-awesome-tab-groupSwitch group of awesome-tab, need install plugin awesome-tab
snails-backend-bufferSearch buffer list
snails-backend-recentfSearch recently files
snails-backend-bookmarkSwitch bookmark
snails-backend-imenuJump to function or variable definition
snails-backend-current-bufferSearch current buffer content
snails-backend-rgUse ripgrep search content in current project, need install ripgrep
snails-backend-search-pdfUse rga search pdf in current directory or supplied directory with @, need install ripgrep-all
snails-backend-fdUse fd search files in current project, need install fd
snails-backend-mdfindUse mdfind search files in local disk, only Mac
snails-backend-everythingUse everything search files in local disk, only Windows, need install everything
snails-backend-fasdUse fasd to search most visit directory , need install fasd
snails-backend-commandSearch command
snails-backend-eaf-pdf-tableSearch table of contents of PDF file, united EAF technology
snails-backend-eaf-browser-historySearch history of browser, united EAF technology, need install fzf
snails-backend-eaf-browser-openOpen url in browser, united EAF technology
snails-backend-eaf-browser-searchOpen keyword in browser, united EAF technology
snails-backend-eaf-github-searchSearch keyword in github, united EAF technology
snails-backend-kv-storeRead/Set/Update/Del key-value pair in sqlite3, need install sqlite3 and required emacsql

Fuzz match

Snails use normal match algorithm default.

Snails will use fuzz match algorithm once you install fuz and add fuz in load-path.

To install fuz.el , please follow below steps:

  1. Install Rust
  2. Download fuz.el repo: git clone https://github.com/cireu/fuz.el
  3. Build fuz-core.so: cargo build --release
  4. Rename target/release/libfuz_core.so or target/release/libfuz_core.dylib to fuz-core.so
  5. Make sure fuz-core.so and all files in https://github.com/cireu/fuz.el add to your load-path

Keymap

KeyDescription
C-nSelect next candidate
C-pSelect previous candidate
M-nSelect next candidate
M-pSelect previous candidate
M-,Select next candidate
M-.Select previous candidate
C-vSelect next backend
M-vSelect previous backend
M-jSelect next backend
M-kSelect previous backend
C-mConfirm candiate
RETConfirm candiate
M-wCopy candidate
C-gQuit snails
ESC ESC ESCQuit snails
M-hQuit snails

Architecture Design of Snails

<img src="./images/framework.png">

snails-core.el is framework code, it only do:

  1. Monitor user input, generate input ticker and send a search request to the backend.
  2. Check backend's search result with input ticker.
  3. Render search result if input ticker is newest.

Sync backend search action is trigger by framework when user type new character. Async backend search action only trigger by framework when user release keyboard key.

Input ticker is the label of the input event,backend's input ticker will expired when user type new character in input buffer.

When backend search finish, framework will drop search result if input ticker is expired.

How to Write a New Plugin?

Writing a plugin for snails is very simple. As long as you have basic knowledge of elisp, you can write a plugin in 5 minutes!

Snails plugins fall into two categories: sync plugins and asynchronous plugins.

Sync plugins are plugins that get the completion results immediately, such as buffers, recent files, etc.

Asynchronous plugins are plugins that take time to get completion results and are usually placed in child processes for calculation, such as find file, grep file, etc.

Writing a Sync Plugin

Let's take the example of snails-backend-recentf plugin:

(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 "")
              (snails-match-input-p input file))
         (snails-add-candiate 'candidates file file)))
     (snails-sort-candidates input candidates 1 1)
     candidates))

 :candidate-icon
 (lambda (candidate)
   (snails-render-file-icon candidate))

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

(provide 'snails-backend-recentf)

Taking the above plug-in as an example, when the user does not input anything, all the recently viewed files are displayed, and when the user does input something, the recently viewed files are filtered according to the input content. When the user confirms, use the find-file command to open the file.

Writing an Async Plugin

Let's take the example of snails-backend-mdfind plugin:

(require 'snails-core)

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

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

 :candidate-filter
 (lambda (candidate-list)
   (let (candidates)
     (dolist (candidate candidate-list)
       (snails-add-candiate 'candidates candidate candidate))
     candidates))

 :candidate-icon
 (lambda (candidate)
   (snails-render-file-icon candidate))

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

(provide 'snails-backend-mdfind)

Taking the above plug-in as an example, when the user inputs "multi-term", build-command will check the input length, and the search will only start after more than 5 characters have been entered. Then, the build-command function will build commands (list "mdfind" "'multi-term'") to pass to the async subprocess. When the async subprocess finishes, it will return a list of strings to the candidate-fitler callback, and the candiate-filter function will wrap the shell result as a candidate list. When the user confirms, use the find-file command to open the file.

Snails is very smart; it will manage subprocesses of the async backend. When the user modifies the input, the snails framework automatically creates a new subprocess to search for the results, while automatically killing the old running process. No matter how fast the user enters, it won't block Emacs.

FAQ

Why doesn't snails frame work when I open a fullscreen Emacs on Mac?

Mac will force the fullscreen Emacs window to a separate workspace, and then any new frame created by make-frame will not float above the Emacs window as expected.

If you start Emacs with fullscreen mode, you can use my workaround code to fix this problem:

(if (featurep 'cocoa)
    (progn
      (setq ns-use-native-fullscreen nil)
      (setq ns-use-fullscreen-animation nil)

      (set-frame-parameter (selected-frame) 'fullscreen 'maximized)

      (run-at-time "2sec" nil
                   (lambda ()
                     (toggle-frame-fullscreen)
                     )))
  (require 'fullscreen)
  (fullscreen))

Why not support match highlight?

Search tools such as fd and rg has --color option. It's easy use `ansi-color' library to render match color.

But the reason why Snails doesn't show highlights is because rendering colors can cause very large performance problems, causing Emacs to get stuck.

The biggest goal of Snails project is fast, although I know that highlighting is very meaningful, so I am willing to sacrifice this feature for fluency.

If you know how to keep fluency when adding highlights, please contribute your patch. ;)

Manage Evil state

I personally never use the evil plugin, if you want manage evil state in input buffer of snails. You should customize your own code with `snails-mode-hook'