-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathcitar-org-roam.el
287 lines (253 loc) · 10.3 KB
/
citar-org-roam.el
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
;;; citar-org-roam.el --- Citar/org-roam integration -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2022-2023 Bruce D'Arcus
;;
;; Author: Bruce D'Arcus <[email protected]>
;; Maintainer: Bruce D'Arcus <[email protected]>
;; Created: May 22, 2022
;; Version: 0.5.1
;; SPDX-License-Identifier: GPL-3.0-or-later
;; SPDX-FileCopyrightText: 2022 Bruce D'Arcus
;; Homepage: https://github.com/emacs-citar/citar-org-roam
;; Package-Requires: ((emacs "27.1") (org-roam "2.2") (citar "1.2.0"))
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; A minor-mode to integrate 'citar' and 'org-roam'.
;;
;; Provides functions for:
;;
;; 1. updating the 'citar' UI from the 'org-roam' database
;; 2. using org-roam to open or create notes
;;
;;; Code:
(require 'org-roam)
(require 'citar)
(defcustom citar-org-roam-subdir nil
"Org-roam subdirectory to place reference notes."
:group 'citar
:group 'citar-org-roam
:type 'string)
(defcustom citar-org-roam-note-title-template
"${author editor}, ${title}"
"The citar template to use for formatting new note titles.
This is the value that is stored as the title in the `org-roam'
database, and displayed in the completion interface."
:group 'citar
:group 'citar-org-roam
:type 'string)
(defcustom citar-org-roam-template-fields
'((:citar-title . ("title"))
(:citar-author . ("author" "editor"))
(:citar-date . ("date" "year" "issued"))
(:citar-pages . ("pages"))
(:citar-type . ("=type=")))
"Field data to include in `org-roam' capture templates.
The `car' of each cons is the property symbol, and the `cdr' the
list of field names to use. When more than one, the value will
be the first result."
:group 'citar
:group 'citar-org-roam
:type '(alist :key-type symbol
:value-type 'list))
(defcustom citar-org-roam-capture-template-key
nil
"When non-nil, use capture template associated with the key.
`citar-org-roam--create-capture-note' will use the template
associated with the key in `org-roam-capture-templates'.
When nil (the default), the template will create an org file in
`citar-org-roam-subdir' named after the citekey with using the
title of the entry as the org title."
:group 'citar
:group 'citar-org-roam
:type 'string)
(defconst citar-org-roam-notes-config
(list :name "Org-Roam Notes"
:category 'org-roam-node
:items #'citar-org-roam--get-candidates
:hasitems #'citar-org-roam-has-notes
:open #'citar-org-roam-open-note
:create #'citar-org-roam--create-capture-note))
(defvar citar-notes-source)
(defvar citar-notes-sources)
(defvar embark-default-action-overrides)
;;; Functions
(defun citar-org-roam--has-note-p (key &optional _entry)
"Return non-nil if a KEY has an associated org-roam ref note."
(let ((ref-node (org-roam-node-from-ref (concat "@" key))))
(when ref-node t)))
(defun citar-org-roam-has-notes ()
"Return function to check for notes.
When given a citekey, return non-nil if there's an associated
note."
;; Lookup performance for this function needs to be as fast as possible, so we
;; use a hash-table.
(let ((hasnotes (make-hash-table :test 'equal)))
(dolist (citekey (citar-org-roam-keys-with-notes))
(puthash citekey t hasnotes))
(lambda (citekey)
(gethash citekey hasnotes))))
(defun citar-org-roam-keys-with-notes ()
"Return a list of keys with associated note(s)."
(mapcar #'car (org-roam-db-query
[:select ref :from refs :where (= type "cite")])))
(defun citar-org-roam-cited (reference)
"Return a list of notes that cite the REFERENCE."
(interactive (list (citar-select-ref
:filter (citar-has-notes))))
(let* ((ids
(org-roam-db-query [:select * :from citations
:where (= cite-key $s1)] reference))
;; TODO one issue on the citar side: the UI has no "has" indicators for
;; these.
;; Also need an annotation for this.
(node-id
(if ids
(completing-read "Note: " ids)
(message "No notes cite this reference."))))
(org-roam-node-visit (org-roam-node-from-id node-id))))
(defun citar-org-roam--node-cite-refs (roam-node)
"Return citation keys in :ROAM_REFS: property of ROAM-NODE."
(seq-filter (lambda (key) (gethash key (citar-get-entries)))
(org-roam-node-refs roam-node)))
(defun citar-org-roam-open-current-refs (&optional prefix)
"Call `citar-open' on all citar cite keys in :ROAM_REFS: property of the node.
If PREFIX is given prompts to select one or more of the cite keys
before calling `citar-open' on them."
(interactive "P")
(if-let ((citar-open-prompt t)
(node (org-roam-node-at-point))
(refs (citar-org-roam--node-cite-refs node)))
(if prefix
(citar-open (citar-select-refs :filter (lambda (key) (member key refs))))
(citar-open refs))
(message "No CiteRefs for this file")))
(defun citar-org-roam-open-note (key-id)
"Open or creat org-roam node for KEY-ID."
(let ((id (substring-no-properties
(car (split-string key-id)))))
(citar-org-roam-open-note-from-id id)))
(defun citar-org-roam-open-note-from-id (node-id)
"Open note from NODE-ID."
(let ((ref-node (org-roam-node-from-id node-id)))
(org-roam-node-visit ref-node)))
(defun citar-org-roam-ref-add ()
"Add a roam_ref to the node at point.
This is just a wrapper for `org-roam-ref-add'."
(interactive)
(let ((ref (citar-select-ref)))
(org-roam-ref-add (concat "@" ref))))
(defun citar-org-roam--get-ref-nodes-for-key (key)
"Return ref node ids for KEY."
(when-let ((ref-node-ids
(org-roam-db-query
[:select [ref node-id] :from refs
:where (= ref $s1)] key)))
ref-node-ids))
(defun citar-org-roam-citekey-for-node-id (node-id)
"Return citekey for NODE-ID."
(caar
(org-roam-db-query
[:select ref :from refs :where (= node-id $s1)] node-id)))
(defun citar-org-roam--get-ref-nodes ()
"Return all ref nodes as id and ref pair."
(when-let ((ref-nodes
(org-roam-db-query
[:select [ref node-id] :from refs])))
ref-nodes))
(defun citar-org-roam--annotate (candidate)
"Annotate the CANDIDATE."
(when-let* ((pair (split-string
(substring-no-properties candidate)))
(nodeid (cadr pair))
(citekey (car pair))
(node (org-roam-node-from-id nodeid))
(ref (org-roam-db-query
[:select ref :from refs
:where (= node-id $s1)] nodeid)))
(propertize (org-roam-node-title node) 'face 'citar)))
(defun citar-org-roam--get-candidates (&optional keys)
"Return ref node candidate list, optionally filtered by KEYS.
Each candidate is a citekey + node-id string, separated by a
space."
(let ((nodes (org-roam-db-query `[:select [refs:node-id refs:ref nodes:title]
:from [refs nodes]
:where (and (= refs:type "cite")
(= refs:node-id nodes:id)
,@(when keys '((in refs:ref $v1))))]
(vconcat keys)))
(cands (make-hash-table :test 'equal)))
(prog1 cands
(pcase-dolist (`(,nodeid ,citekey ,title) nodes)
;; TODO include note title in the candidate string?
(push
(concat
(propertize nodeid 'invisible t) " ["
(propertize citekey 'face 'citar-highlight)
(truncate-string-to-width "] " (- 60 (length citekey)) nil 32)
(propertize title 'face 'citar))
(gethash citekey cands))))))
(defun citar-org-roam--make-info-plist (citekey)
"Return org-roam capture template plist for CITEKEY."
(let ((infoplist))
(seq-do
(pcase-lambda (`(,capturevar . ,citarvars))
;; REVIEW do we only want to do this when non-nil?
(setq infoplist
(plist-put infoplist capturevar
(cdr (citar-get-field-with-value
citarvars citekey)))))
citar-org-roam-template-fields)
(setq infoplist
(plist-put infoplist :citar-citekey citekey))
infoplist))
(defun citar-org-roam--create-capture-note (citekey entry)
"Open or create org-roam node for CITEKEY and ENTRY."
;; adapted from https://jethrokuan.github.io/org-roam-guide/#orgc48eb0d
(let* ((notetitle (citar-format--entry
citar-org-roam-note-title-template entry))
(templatekey citar-org-roam-capture-template-key)
(infoplist (citar-org-roam--make-info-plist citekey)))
(apply 'org-roam-capture-
:info (setq infoplist
;; Add notetitle in case someone wants to use it in their
;; capture template.
(plist-put infoplist :note-title notetitle))
:node (org-roam-node-create :title notetitle)
:props '(:finalize find-file)
(if templatekey
(list :keys templatekey)
(list
:templates
'(("r" "reference" plain "%?" :if-new
(file+head
"%(concat
(when citar-org-roam-subdir (concat citar-org-roam-subdir \"/\")) \"${citar-citekey}.org\")"
"#+title: ${note-title}\n")
:immediate-finish t
:unnarrowed t)))))
(org-roam-ref-add (concat "@" citekey))))
(defvar citar-org-roam--orig-source citar-notes-source)
(defun citar-org-roam-setup ()
"Setup `citar-org-roam-mode'."
;; This seems to require running if citar is loaded before org-roam.
(org-roam-db-sync)
(citar-register-notes-source
'citar-org-roam citar-org-roam-notes-config)
(setq citar-notes-source 'citar-org-roam))
(defun citar-org-roam-reset ()
"Reset `citar-org-roam-mode' to default."
(setq citar-notes-source citar-org-roam--orig-source)
(citar-remove-notes-source 'citar-org-roam))
;;;###autoload
(define-minor-mode citar-org-roam-mode
"Toggle `citar-org-roam-mode'."
:global t
:group 'citar
:lighter " citar-org-roam"
(if citar-org-roam-mode (citar-org-roam-setup)
(citar-org-roam-reset)))
(provide 'citar-org-roam)
;;; citar-org-roam.el ends here