0%

在Emacs中搭建笔记查阅系统的尝试

给Emacs写插件有种痛并快乐着的感觉。虽然这个发挥创意的过程很有趣,但是Elisp写起来总有种别扭的感觉。一方面,我把它当成是Common Lisp,写的时候没有觉得“这个用法可能会有问题”;另一方面,它又不是普通的写lisp代码,还要一边写一边摸索Emacs中的一些概念。不过总体而言,还是挺好玩的,除了没有一个像模像样的REPL之外。

来龙去脉

我用Emacs记录了不少的“笔记”。虽说我自己将其称为笔记,但是它们更像是我把遇到的一些问题和解决方法给记录下来,而没有太多自己的感悟。它们的外观倒是高度的一致,见下图

(第一次尝试给自己的图片打水印,有点好玩)每一个一级条目都是一个问题,并且这个文件中只有一级条目。而条目下的内容则是对标题的问题的回答。其中还有代码块——也就是写着BEGIN_SRC和END_SRC的那部分。用org-mode来记录笔记有几个好处,其中一个便是可以在笔记中插入任何Emacs支持的编程语言代码片段并具备语法高亮。当然了,还有一个巨大的优势,便是org-mode尽管看似花里胡哨,骨子里却是正统的纯文本文件,它可以很方便地在其它工具中处理。

而我用来处理的其中一个工具便是ElasticSearch。比如说,上图的第一条笔记,在ElasticSearch中存成了下面这样的结构

本来我是写了一个Alfred的Workflow来查询ElasticSearch的,但是奈何Workflow那种一行行的方式展示org-mode格式的笔记不太友好,因此便打算直接在Emacs中查询并查看笔记内容。

牛刀小试

为了可以在Emacs中查看笔记内容,我打算借助于Helm的力量。Helm是Emacs的一个补全的框架,可以用来呈现一系列的候选项,然后选中后触发一些什么动作。我期望的形式,是在Emacs中按下某种快捷键或者输入某个命令行,可以在minibuffer中输入自己要查询的内容,然后Emacs查询ElasticSearch并最终通过Helm来呈现这些查询内容匹配的笔记条目。目前的成果是下面这样子的

具体的做法其实也很简单。首先,要知道Helm是如何被使用的。通过这篇文档,初步了解到只需要定一个变量,并通过:sources关键字参数传递给helm这个函数即可。我所定义的传递给helm函数的“source”如下

1
2
3
4
5
6
(setq faq-helm-sources
`((name . "FAQ at Emacs")
(candidates . faq-candidates)
(action . (lambda (candidate)
(let ((url (format "http://localhost:9200/faq/_doc/%s" candidate)))
(browse-url url))))))

其中faq-candidates的作用便是根据minibuffer中的关键字查询ElasticSearch并组织好一个结构返回给helm。需要注意的是,faq-candidates必须是一个无参的函数才行,但输入的数据又偏偏需要从minibuffer中获取。因此,我的做法是约定一个变量faq-query,在调用helm之前首先调用read-from-minibuffer函数读取输入,然后将输入的字符串赋值给faq-query,之后当helm开始使用这个source的时候,faq-candidates函数便不需要参数,而可以直接从faq-query中拿到自己需要的搜索内容向ElasticSearch请求了。当然了,如果有像Common Lisp动态作用域的话,也就不需要定义这么一个全局变量了,对Emacs全局的侵入会更少一点。

目前能够做到的也仅仅是查询ElasticSearch,并在选中某个条目并按下回车的时候打开浏览器来查看而已,之后应该会继续完善。目前的完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
;;; 调用ElasticSearch查询笔记
(require 'request)

(defun faq (query)
"向ElasticSearch查询QUERY匹配的笔记"
(let ((response))
(request
"http://localhost:9200/faq/_search"
:data (encode-coding-string
(json-encode
(list
(cons "query" (list
(cons "multi_match" (list
(cons "fields" (list "answer" "question"))
(cons "query" query)))))))
'utf-8)
:headers '(("Content-Type" . "application/json"))
:parser 'buffer-string
:success (cl-function
(lambda (&key data &allow-other-keys)
(setq data (decode-coding-string data 'utf-8))
(setq response (json-read-from-string data))))
:sync t)
response))

(defun make-faq-candidates (response)
"将查询ElasticSearch的结果构造为helm可以识别的candidates格式"
(let ((hits (cdr (assoc 'hits (cdr (assoc 'hits response))))))
(mapcar (lambda (doc)
(let ((_source (cdr (assoc '_source doc))))
(cons (cdr (assoc 'question _source))
(cdr (assoc '_id doc)))))
hits)))

(defvar faq-query nil)

(defun faq-candidates ()
(make-faq-candidates (faq faq-query)))

(setq faq-helm-sources
`((name . "FAQ at Emacs")
(candidates . faq-candidates)
(action . (lambda (candidate)
(let ((url (format "http://localhost:9200/faq/_doc/%s" candidate)))
(browse-url url))))))

(defun lt-ask ()
"交互式地从minibuffer中读取笔记的关键词并展示选项"
(interactive)
(let ((content (read-from-minibuffer "笔记关键词:")))
(setq faq-query content)
(helm :sources '(faq-helm-sources))))

有不少值得吐槽的地方,不过都先按下不表吧,各位读者有兴趣的话可以留言交流一下XD

Liutos wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
你的一点心意,我的十分动力。