在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”如下

(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,並在選中某個條目並按下回車的時候打開瀏覽器來查看而已,之後應該會繼續完善。目前的完整代碼如下

;;; 調用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

閱讀原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章