优化counsel-bookmark
Emacs的书签非常好用,通过它可以非常快速地跳转到对应位置。虽然使用 ivy-switch-buffer
已经可以非常快速地切换文件。但是书签跳转还要更高效一些:
-
书签精确到行级别,通过书签可以直接跳到某个文件的某一行。也可以给不同的行设置书签,然后在不同的行之间来回跳转。
-
有些文件路径非常深,记不住文件名和路径,不能通过
ivy-switch-buffer
快速的找到。这时候就可以记录一个书签,起个好记忆的名字,或者给书签打上tag。
Emacs原生书签功能相对简单,因此我使用了 BookmarkPlus,它在原生的基础上做了很多扩展,比如支持给书签打tag,支持函数的书签,支持书签的注释等等。
随着日常使用的积累,书签越来越多,书签的搜索成了一个大问题。一直用 counsel-bookmark
来搜索书签,但它只能按书签名称进行搜索:
书签多了之后经常忘记名称,找不到想要的书签,counsel-bookmark的功能就显得有些鸡肋。其实除了书签的名称之外,文件路径、书签tag都是非常重要的信息,如果把这些内容综合起来再搜索,只需要记得其中一项就可以匹配,效率会高很多。
后来发现了 ivy-rich 和 all-the-icons-ivy-rich 这两个包,使用后搜索列表增加了icon、书签类型、文件路径等信息:
比counsel-bookmark好一些了,但仍然不能按文件路径进行搜索。因为ivy-rich只是修改了列表展示,并没有修改原始的候选列表。因此产生了优化counsel-bookmark的想法,实现这几点功能:
-
在搜索列表中保留icon、文件路径、标签等丰富的信息。
-
文件路径可以搜索。
-
增加书签tag展示,支持按tag搜索书签。
优化思路
翻一下counsel-bookmark的代码,发现它的逻辑很简单:通过 ivy-read
让用户选择需要跳转到的书签(这里是把书签名称用作候选列表),再调用 bookmark-jump
函数跳转。
我们的目标就很明确了: 自定义 ivy-read
的候选列表,把丰富的信息展示出来,在用户选择之后,通过原始书签记录进行跳转。 但如果ivy-read接受的是字符串的列表,我们如何把字符串和原始的书签数据绑定到一起呢?通过 ivy-read 文档了解到它不仅可以接受字符串列表,还可以接受alist。
做一个简单的实验:
(ivy-read "x: " '(("a" . 1) ("b" . 2))
:action (lambda (x) (message "%s" x)))
⇒ (a 1)
运行上面这段代码,如果选择了a,则输出结果为 (a . 1)
。因此我们可以构造一个alist传递给ivy-read,key是包含丰富信息的字符串,value是原始的书签数据。
构造候选列表
我们想要构造的字符串要包含这些内容:书签名称、文件的完整路径、tag以及icon,前面三个都可以在emacs的书签数据拿到。emacs的书签存储在 bookmark-alist
变量中,用 (car bookmark-alist)
看一下第一个元素:
(#("jxq-run-bazel" 0 13 (bmkp-full-record #0))
(tags "emacs" "init")
(filename . "~/code/jxq_config/emacs/config.org")
(buffer-name . "config.org")
(front-context-string . "(defun jxq-run-b")
(rear-context-string . "compile cmd)))
")
(front-context-region-string)
(rear-context-region-string)
(visits . 8)
(time 25561 58133 43415 0)
(created 25560 54274 926647 0)
(position . 88091))
这就是一个 alist,可以用 alist-get
方法查找对应的数据。
(alist-get 'tags (car bookmark-alist))
⇒ ("emacs" "init")
(alist-get 'filename (car bookmark-alist))
⇒ "~/code/jxq_config/emacs/config.org"
至于书签icon,可以直接使用 all-the-icons-ivy-rich-bookmark-icon ,我是基于它的代码做了一些小修改,参考下面的 jxq-bookmark-icon
函数。
效果
最终实现效果如下图所示。与开启了ivy-rich的版本相比,一个比较明显的变化是增加了tag。另一个变化是整个候选列表中的内容都可以用来匹配,比如图里第一个书签文件名称是rust-run,文件路径是rust-cargo.el,在原生的版本中是无法通过 cargo
这个关键词来匹配的。
完整实现代码
(defun jxq-bookmark-icon (bmk)
(all-the-icons-ivy-rich--format-icon
(let ((file (alist-get 'filename bmk)))
(cond
((alist-get 'function bmk)
(all-the-icons-fileicon "elisp"))
((null file)
(all-the-icons-material "block"
:height 1.0
:v-adjust -0.2
:face 'all-the-icons-ivy-rich-warn-face))
((file-remote-p file)
(all-the-icons-octicon "radio-tower" :height 0.8 :v-adjust 0.01))
((not (file-exists-p file))
(all-the-icons-material "block"
:height 1.0 :v-adjust -0.2
:face 'all-the-icons-ivy-rich-error-face))
((file-directory-p file)
(all-the-icons-octicon "file-directory" :height 0.9 :v-adjust 0.01))
(t (all-the-icons-icon-for-file
(file-name-nondirectory file) :height 0.9 :v-adjust 0.0))))))
(defun jxq-normalize-string (str len)
(let ((str-len (string-width str)))
(cond ((< str-len len)
(concat str (make-string (- len str-len) ? )))
((> str-len len)
(s-reverse (s-truncate len (s-reverse str) "…")))
(t str))))
(defun jxq-bookmark-item (bmk)
(let* ((total-width (window-width (minibuffer-window)))
(sep (make-string 5 ?\s))
(name-width (floor (* 0.25 total-width)))
(filename-width (floor (* 0.5 total-width)))
(raw-filename (cdr (assoc 'filename bmk)))
(name (jxq-normalize-string
(substring-no-properties (car bmk)) name-width))
(filename (propertize
(jxq-normalize-string
(or raw-filename "") filename-width)
'face font-lock-doc-face))
(tags (propertize
(jxq-normalize-string
(mapconcat
(lambda (tag) (concat "#" tag))
(alist-get 'tags bmk)
" ")
20)
'face font-lock-keyword-face))
(icon (jxq-bookmark-icon bmk)))
(list (concat icon "\t" name sep filename sep tags) bmk)))
(defun jxq-bookmarks-list ()
(interactive)
(mapcar (lambda (bmk)
(jxq-bookmark-item bmk))
bookmark-alist))
(defun jxq-counsel-bookmark ()
"counsel-bookmark plus"
(interactive)
(ivy-read "bookmark: "
(jxq-bookmarks-list)
:action
(lambda (x)
(pcase x
(`(,name ,bmk) (bookmark-jump bmk))
(_ (bookmark-set x))))))
(define-key modalka-mode-map (kbd "jb") #'jxq-counsel-bookmark)