org-velocity.el 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. ;;; org-velocity.el --- something like Notational Velocity for Org.
  2. ;; Copyright (C) 2010 Paul M. Rodriguez
  3. ;; Author: Paul M. Rodriguez <paulmrodriguez@gmail.com>
  4. ;; Created: 2010-05-05
  5. ;; Version: 2.1
  6. ;; This file is not part of GNU Emacs.
  7. ;; This program is free software; you can redistribute it and/or
  8. ;; modify it under the terms of the GNU General Public License as
  9. ;; published by the Free Software Foundation version 2.
  10. ;; This program is distributed in the hope that it will be useful, but
  11. ;; WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. ;; General Public License for more details.
  14. ;; For a copy of the GNU General Public License, search the Internet,
  15. ;; or write to the Free Software Foundation, Inc., 59 Temple Place,
  16. ;; Suite 330, Boston, MA 02111-1307 USA
  17. ;;; Commentary:
  18. ;; Org-Velocity.el implements an interface for Org inspired by the
  19. ;; minimalist notetaking program Notational Velocity. The idea is to
  20. ;; allow you to maintain, amass and access brief notes on many
  21. ;; subjects with minimal fuss.
  22. ;; It can be used in two ways: to store and access notes from any
  23. ;; buffer a universal bucket file; or as a method for navigating any
  24. ;; Org file.
  25. ;; The name of the bucket-file (`org-velocity-bucket') and whether to
  26. ;; always use it (`org-velocity-always-use-bucket-file') are set
  27. ;; through Customize. If the bucket file is set but not always to be
  28. ;; used, then calling Org-Velocity outside of Org-mode uses the bucket
  29. ;; file; calling it in Org mode uses the current buffer. If no bucket
  30. ;; file is set then Org-Velocity only works when called from Org.
  31. ;; Even if the bucket file is always to be used, calling
  32. ;; `org-velocity-read' with an argument will use the current file.
  33. ;; The interface, unlike its inspiration, is not incremental.
  34. ;; Org-Velocity prompts for search terms in the usual way; if the user
  35. ;; has customized `org-velocity-use-completion', completion is offered
  36. ;; on the headings in the target file. If the search multiple times
  37. ;; in the target file, a buffer containing a buttonized list of the
  38. ;; headings where it occurs is displayed. Results beyond what can be
  39. ;; indexed are discarded. After clicking on a heading, or typing a
  40. ;; character associated with it, the user is taken to the heading.
  41. ;; (Typing 0 forces a new heading to be created.) If
  42. ;; `org-velocity-edit-indirectly' is so set, the heading and its
  43. ;; subtree are displayed in an indirect buffer. Otherwise the user is
  44. ;; simply taken to the proper buffer and position.
  45. ;; If the user simply hits RET at the prompt, without making a choice,
  46. ;; then the search is restored for editing. A blank search quits.
  47. ;; This method of selection is obviously not as slick as the original,
  48. ;; but probably more useful for a keyboard-driven interface.
  49. ;; If the search does not occur in the file the user is offered a
  50. ;; choice to create a new heading named with the search. When
  51. ;; org-remember is loaded, or the user customizes
  52. ;; `org-velocity-use-remember', then org-remember is used to insert
  53. ;; the new heading. Otherwise the user is simply taken to a new
  54. ;; heading at the end of the file.
  55. ;; Thanks to Richard Riley, Carsten Dominik, and Bastien Guerry for
  56. ;; their suggestions.
  57. ;;; Usage:
  58. ;;; (require 'org-velocity)
  59. ;;; (setq org-velocity-bucket (concat org-directory "/bucket.org"))
  60. ;;; (global-set-key (kbd "C-c v") 'org-velocity-read)
  61. ;;; Code:
  62. (require 'org)
  63. (require 'button)
  64. (eval-when-compile (require 'cl))
  65. (defgroup org-velocity nil
  66. "Notational Velocity-style interface for Org."
  67. :tag "Org-Velocity"
  68. :group 'outlines
  69. :group 'hypermedia)
  70. (defcustom org-velocity-bucket ""
  71. "Where is the bucket file?"
  72. :group 'org-velocity
  73. :type 'file)
  74. (defcustom org-velocity-always-use-bucket nil
  75. "Use bucket file even when called from an Org buffer?"
  76. :group 'org-velocity
  77. :type 'boolean)
  78. (defcustom org-velocity-use-completion nil
  79. "Complete on heading names?"
  80. :group 'org-velocity
  81. :type 'boolean)
  82. (defcustom org-velocity-use-remember (featurep 'org-remember)
  83. "Use Org-remember or just visit the file?"
  84. :group 'org-velocity
  85. :type 'boolean)
  86. (defcustom org-velocity-remember-method 'bottom
  87. "Where in files should `org-remember' record new entries?"
  88. :group 'org-velocity
  89. :type '(choice (const :tag "Add at bottom" bottom)
  90. (const :tag "Add at top" top)
  91. (const :tag "Use date tree" date-tree)))
  92. (defcustom org-velocity-edit-indirectly t
  93. "Edit entries in an indirect buffer or just visit the file?"
  94. :group 'org-velocity
  95. :type 'boolean)
  96. (defcustom org-velocity-search-method 'phrase
  97. "Match on whole phrase, any word, or all words?"
  98. :group 'org-velocity
  99. :type '(choice
  100. (const :tag "Match whole phrase" phrase)
  101. (const :tag "Match any word" any)
  102. (const :tag "Match all words" all)))
  103. (defcustom org-velocity-allow-regexps nil
  104. "Allow searches to use regular expressions?"
  105. :group 'org-velocity
  106. :type 'boolean)
  107. (defvar org-velocity-index
  108. (nconc (number-sequence 49 57) ;numbers
  109. (number-sequence 97 122) ;lowercase letters
  110. (number-sequence 65 90)) ;uppercase letters
  111. "List of chars for indexing results.")
  112. (defstruct (org-velocity-heading
  113. (:constructor org-velocity-make-heading)
  114. (:type list))
  115. (marker (point-marker))
  116. (name (substring-no-properties
  117. (org-get-heading))))
  118. (defun org-velocity-use-file ()
  119. "Return the proper file for Org-Velocity to search.
  120. If `org-velocity-always-use-bucket' is t, use bucket file; complain
  121. if missing. Otherwise if this is an Org file, use it."
  122. (let ((org-velocity-bucket
  123. (and org-velocity-bucket (expand-file-name org-velocity-bucket))))
  124. (if org-velocity-always-use-bucket
  125. (or org-velocity-bucket (error "Bucket required but not defined"))
  126. (if (and (eq major-mode 'org-mode)
  127. (buffer-file-name))
  128. (buffer-file-name)
  129. (or org-velocity-bucket
  130. (error "No bucket and not an Org file"))))))
  131. (defsubst org-velocity-display-buffer ()
  132. "Return the proper buffer for Org-Velocity to display in."
  133. (get-buffer-create "*Velocity headings*"))
  134. (defsubst org-velocity-bucket-buffer ()
  135. "Return proper buffer for bucket operations."
  136. (find-file-noselect (org-velocity-use-file)))
  137. (defun org-velocity-quote (search)
  138. "Quote SEARCH as a regexp if `org-velocity-allow-regexps' is non-nil.
  139. Acts like `regexp-quote' on a string, `regexp-opt' on a list."
  140. (if org-velocity-allow-regexps
  141. search
  142. (if (listp search)
  143. (regexp-opt search)
  144. (regexp-quote search))))
  145. (defun org-velocity-nearest-heading (position)
  146. "Return last heading at POSITION.
  147. If there is no last heading, return nil."
  148. (save-excursion
  149. (goto-char position)
  150. (unless (org-before-first-heading-p)
  151. (org-back-to-heading)
  152. (org-velocity-make-heading))))
  153. (defun org-velocity-make-button-action (heading)
  154. "Return a form to visit HEADING."
  155. `(lambda (button)
  156. (run-hooks 'mouse-leave-buffer-hook) ;turn off temporary modes
  157. (if org-velocity-edit-indirectly
  158. (org-velocity-edit-entry ',heading)
  159. (progn
  160. (message "%s" ,(org-velocity-heading-name heading))
  161. (switch-to-buffer (marker-buffer
  162. ,(org-velocity-heading-marker heading)))
  163. (goto-char (marker-position
  164. ,(org-velocity-heading-marker heading)))))))
  165. (defun org-velocity-edit-entry (heading)
  166. "Edit entry at HEADING in an indirect buffer."
  167. (let ((buffer (make-indirect-buffer
  168. (marker-buffer (org-velocity-heading-marker heading))
  169. (generate-new-buffer-name
  170. (org-velocity-heading-name heading)))))
  171. (with-current-buffer buffer
  172. (let ((org-inhibit-startup t))
  173. (org-mode))
  174. (goto-char (marker-position (org-velocity-heading-marker heading)))
  175. (narrow-to-region (point)
  176. (save-excursion
  177. (org-end-of-subtree)
  178. (point)))
  179. (goto-char (point-min))
  180. (add-hook 'org-ctrl-c-ctrl-c-hook 'org-velocity-dismiss nil t))
  181. (pop-to-buffer buffer)
  182. (message "%s" "Use C-c C-c to save changes.")))
  183. (defun org-velocity-dismiss ()
  184. "Save current entry and close indirect buffer."
  185. (progn
  186. (save-buffer)
  187. (kill-buffer)))
  188. (defun org-velocity-buttonize (heading)
  189. "Insert HEADING as a text button."
  190. (insert (format "#%c " (nth (1- (line-number-at-pos)) org-velocity-index)))
  191. (insert-text-button
  192. (org-velocity-heading-name heading)
  193. 'action (org-velocity-make-button-action heading))
  194. (newline))
  195. (defun org-velocity-insert-heading (heading)
  196. "Add a new heading named HEADING."
  197. (with-current-buffer (org-velocity-bucket-buffer)
  198. (goto-char (point-max))
  199. (newline)
  200. (org-insert-heading) (insert heading)
  201. (newline)
  202. (goto-char (point-max))))
  203. (defun org-velocity-remember (heading &optional region)
  204. "Use `org-remember' to record a note to HEADING.
  205. If there is a REGION that will be inserted."
  206. (let ((org-remember-templates
  207. (list (list
  208. "Velocity entry"
  209. ?v
  210. (let ((string "* %s\n\n%%?"))
  211. (if region
  212. (format (concat string "%s") heading region)
  213. (format string heading)))
  214. (org-velocity-use-file)
  215. org-velocity-remember-method))))
  216. (org-remember nil ?v)))
  217. (defun org-velocity-all-search (search)
  218. "Return entries containing all words in SEARCH."
  219. (when (file-exists-p (org-velocity-use-file))
  220. (save-excursion
  221. (delq nil
  222. (let ((keywords
  223. (mapcar 'org-velocity-quote
  224. (split-string search)))
  225. (case-fold-search t))
  226. (apply 'nconc
  227. (org-map-entries
  228. (lambda ()
  229. (let ((limit (save-excursion (org-end-of-subtree)
  230. (point))))
  231. (catch 'fail
  232. (mapcar
  233. (lambda (word)
  234. (or (save-excursion
  235. (and (re-search-forward word limit t)
  236. (org-velocity-nearest-heading
  237. (match-beginning 0))))
  238. (throw 'fail nil)))
  239. keywords)))))))))))
  240. (defun org-velocity-generic-search (search)
  241. "Return entries containing SEARCH."
  242. (save-excursion
  243. (delq nil
  244. (nreverse
  245. (let (matches (case-fold-search t))
  246. (goto-char (point-min))
  247. (while (re-search-forward search
  248. (point-max) t)
  249. (push (org-velocity-nearest-heading (match-beginning 0))
  250. matches)
  251. (outline-next-heading))
  252. matches)))))
  253. (defsubst org-velocity-phrase-search (search)
  254. "Return entries containing SEARCH as a phrase."
  255. (org-velocity-generic-search (org-velocity-quote search)))
  256. (defsubst org-velocity-any-search (search)
  257. "Return entries containing any word in SEARCH."
  258. (org-velocity-generic-search (org-velocity-quote (split-string search))))
  259. (defun org-velocity-present (headings)
  260. "Buttonize HEADINGS in `org-velocity-display-buffer'."
  261. (and (listp headings) (delete-dups headings))
  262. (let ((cdr (nthcdr
  263. (1- (length org-velocity-index))
  264. headings)))
  265. (and (consp cdr) (setcdr cdr nil)))
  266. (with-current-buffer (org-velocity-display-buffer)
  267. (mapc
  268. 'org-velocity-buttonize
  269. headings)
  270. (goto-char (point-min))))
  271. (defun org-velocity-new (search &optional ask)
  272. "Create new heading named SEARCH.
  273. If ASK is non-nil, ask first."
  274. (if (or (null ask)
  275. (y-or-n-p "No match found, create? "))
  276. ;; if there's a region, we want to insert it
  277. (let ((region (if (use-region-p)
  278. (buffer-substring
  279. (region-beginning)
  280. (region-end)))))
  281. (if org-velocity-use-remember
  282. (org-velocity-remember search region)
  283. (progn
  284. (org-velocity-insert-heading search)
  285. (switch-to-buffer (org-velocity-bucket-buffer))
  286. (when region (insert region))))
  287. (when region (message "%s" "Inserted region"))
  288. search)))
  289. (defun org-velocity-engine (search)
  290. "Display a list of headings where SEARCH occurs."
  291. (with-current-buffer (org-velocity-display-buffer) (erase-buffer))
  292. (unless (string-equal "" search);exit on empty string
  293. (case
  294. (with-current-buffer (org-velocity-bucket-buffer)
  295. (save-excursion
  296. (let ((matches
  297. (case org-velocity-search-method
  298. ('phrase (org-velocity-phrase-search search))
  299. ('any (org-velocity-any-search search))
  300. ('all (org-velocity-all-search search)))))
  301. (org-velocity-present matches)
  302. (cond ((zerop (length matches)) 'new)
  303. ((= (length matches) 1) 'follow)
  304. ((> (length matches) 1) 'prompt)))))
  305. ('prompt (progn
  306. (display-buffer (org-velocity-display-buffer))
  307. (case (org-velocity-follow-hint)
  308. ('edit (org-velocity-read nil search))
  309. ('new (org-velocity-new search)))))
  310. ('new (unless (org-velocity-new search t)
  311. (org-velocity-read nil search)))
  312. ('follow (if (y-or-n-p "One match, follow? ")
  313. (progn
  314. (set-buffer (org-velocity-display-buffer))
  315. (goto-char (point-min))
  316. (button-activate (next-button (point))))
  317. (org-velocity-read nil search))))))
  318. (defun org-velocity-list-position (elt list)
  319. "Return first position of ELT in LIST"
  320. (let ((copy (copy-list list)))
  321. (1-
  322. (length
  323. (progn
  324. (setcdr (member elt copy) nil)
  325. copy)))))
  326. (defun org-velocity-activate-button (char)
  327. "Go to button on line number associated with CHAR in `org-velocity-index'."
  328. (goto-char (point-min))
  329. (forward-line (org-velocity-list-position char org-velocity-index))
  330. (goto-char
  331. (button-start
  332. (next-button (point))))
  333. (message "%s" (button-label (button-at (point))))
  334. (button-activate (button-at (point))))
  335. (defun org-velocity-follow-hint ()
  336. "Prompt for index of button."
  337. (let ((hint
  338. (read-key
  339. "Follow (0 for new note, RET to edit search, TAB to scroll): ")))
  340. (cond
  341. ;; quit?
  342. ((or (eq hint 7) ;C-g
  343. (eq hint 27)) ;ESC
  344. (keyboard-quit))
  345. ;; zero?
  346. ((eq hint 48)
  347. 'new)
  348. ;; return?
  349. ((or (eq hint 13) ;\r
  350. (eq hint 10)) ;\n
  351. 'edit)
  352. ;; tab?
  353. ((eq hint 9)
  354. (let ((other-window-scroll-buffer
  355. (org-velocity-display-buffer)))
  356. (scroll-other-window))
  357. (org-velocity-follow-hint))
  358. ;; click?
  359. ((mouse-event-p hint)
  360. (mouse-set-point hint)
  361. (if (button-at (point))
  362. (push-button (point))
  363. (org-velocity-follow-hint)))
  364. ;; unhandled char?
  365. ((not (memq hint org-velocity-index))
  366. (org-velocity-follow-hint))
  367. ;; index beyond results?
  368. ((> (org-velocity-list-position hint org-velocity-index)
  369. (with-current-buffer (org-velocity-display-buffer)
  370. (1- (count-lines (point-min) (point-max)))))
  371. (org-velocity-follow-hint))
  372. ;; follow hint
  373. (t (set-buffer (org-velocity-display-buffer))
  374. (org-velocity-activate-button hint)))))
  375. (defun org-velocity-read-string (prompt &optional initial-input)
  376. "Read string using `read-string', with PROMPT followed by INITIAL-INPUT."
  377. ;; The use of initial inputs to the minibuffer is deprecated (see
  378. ;; `read-from-minibuffer', but in this case it is the user-friendly
  379. ;; thing to do.
  380. (let ((minibuffer-setup-hook minibuffer-setup-hook))
  381. (add-hook 'minibuffer-setup-hook (lambda ()
  382. (and initial-input (insert initial-input))
  383. (goto-char (point-max))))
  384. (if (and org-velocity-use-completion
  385. ;; map-entries complains for nonexistent files
  386. (file-exists-p (org-velocity-use-file)))
  387. (completing-read
  388. prompt
  389. (with-current-buffer (org-velocity-bucket-buffer)
  390. (org-map-entries
  391. (lambda ()
  392. (substring-no-properties
  393. (org-get-heading))))))
  394. (read-string prompt))))
  395. (defun org-velocity-read (arg &optional search)
  396. "Read a search string SEARCH for Org-Velocity interface.
  397. This means that a buffer will display all headings where SEARCH
  398. occurs, where one can be selected by a mouse click or by typing
  399. its index. If SEARCH does not occur, then a new heading may be
  400. created named SEARCH.
  401. If `org-velocity-bucket' is defined and
  402. `org-velocity-always-use-bucket' is non-nil, then the bucket file
  403. will be used; otherwise, this will work when called in any Org
  404. file. Calling with ARG forces current file."
  405. (interactive "P")
  406. (let ((org-velocity-always-use-bucket
  407. (if arg nil org-velocity-always-use-bucket)))
  408. ;; complain if inappropriate
  409. (assert (org-velocity-use-file))
  410. (unwind-protect
  411. (org-velocity-engine
  412. (org-velocity-read-string "Velocity search: " search))
  413. (progn
  414. (kill-buffer (org-velocity-display-buffer))
  415. (delete-other-windows)))))
  416. (provide 'org-velocity)
  417. ;;; org-velocity.el ends here