blob: c367635aebc18b8300273ff07ced371c6ea473d3 (
plain) (
blame)
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
|
;;; denote-capf.el --- completion-at-point for denote -*- lexical-binding: t; -*-
;;; Commentary:
;; `denote-capf' provides `completion-at-point' functions for Denote
;; note titles and keywords. Enable it in Org buffers with:
;;
;; (add-hook 'org-mode-hook #'denote-capf-setup)
;;
;; Or call `M-x denote-capf-setup' manually. Use `M-x
;; denote-capf-teardown' to disable it in the current buffer, and
;; `M-x denote-capf-refresh' to rebuild the cache.
;;; Code:
(require 'denote)
(require 'subr-x)
(require 'seq)
(require 'thingatpt)
(declare-function org-in-src-block-p "org")
(defvar corfu-auto)
(defgroup denote-capf nil
"Completion-at-point functions for Denote links and keywords."
:group 'denote)
(defcustom denote-capf-min-prefix 3
"Minimum length of the word at point before offering note completions."
:type 'integer :group 'denote-capf)
(defcustom denote-capf-include-encrypted nil
"When non-nil, include encrypted notes in the completion candidates."
:type 'boolean :group 'denote-capf)
(defcustom denote-capf-directories nil
"Directories whose org files get denote completion-at-point.
When nil, `denote-capf-setup' enables completion in every org
buffer. When set, only files living under one of these
directories are touched, leaving every other org buffer alone."
:type '(repeat directory) :group 'denote-capf)
(defvar denote-capf--titles nil)
(defvar denote-capf--title->id nil)
(defvar denote-capf--timer nil)
(defun denote-capf--build ()
"Build the list of denote note titles and their identifier mapping."
(let ((titles '())
(table (make-hash-table :test 'equal)))
(dolist (file (denote-directory-files nil nil t (unless denote-capf-include-encrypted "\\.\\(gpg\\|age\\)\\'")))
(let ((id (denote-retrieve-filename-identifier file))
(title (denote-retrieve-title-or-filename file 'org)))
(when (and id title (not (string-empty-p title)))
(unless (gethash title table) (push title titles))
(puthash title id table))))
(setq denote-capf--titles (nreverse titles)
denote-capf--title->id table)))
(defun denote-capf--ensure ()
"Build the completion cache if it is not already populated."
(unless denote-capf--title->id (denote-capf--build)))
(defun denote-capf--schedule ()
"Schedule a rebuild of the completion cache during idle time."
(when (timerp denote-capf--timer) (cancel-timer denote-capf--timer))
(setq denote-capf--timer (run-with-idle-timer 0.5 nil #'denote-capf--build)))
(defun denote-capf-refresh ()
"Rebuild the denote completion cache.
When called interactively, show how many notes were cached."
(interactive)
(denote-capf--build)
(when (called-interactively-p 'any)
(message "Denote completion cache refreshed (%d notes)"
(length denote-capf--titles))))
(defun denote-capf--invalidate ()
"Clear the completion cache and schedule a rebuild."
(setq denote-capf--title->id nil denote-capf--titles nil)
(denote-capf--schedule))
(add-hook 'denote-after-new-note-hook #'denote-capf--invalidate)
(add-hook 'denote-after-rename-file-hook #'denote-capf--invalidate)
(defun denote-capf--in-link-p ()
"Return non-nil when point is inside an Org link."
(save-excursion
(let ((pos (point)) (bol (line-beginning-position)) (depth 0))
(goto-char bol)
(while (< (point) pos)
(cond ((looking-at "\\[\\[") (setq depth (1+ depth)) (forward-char 2))
((looking-at "\\]\\]") (when (> depth 0) (setq depth (1- depth))) (forward-char 2))
(t (forward-char 1))))
(> depth 0))))
(defun denote-capf--link-exit (str status)
"Exit function for `denote-capf-link'.
Replace the completed title with a denote link when STATUS is one
of finished, exact or sole."
(when (memq status '(finished exact sole))
(when-let* ((id (gethash str denote-capf--title->id)))
(delete-region (- (point) (length str)) (point))
(insert (format "[[denote:%s][%s]]" id str)))))
(defun denote-capf-link ()
"Completion-at-point function for denote note links."
(when (and (derived-mode-p 'org-mode)
(not (org-in-src-block-p))
(not (save-excursion (beginning-of-line) (looking-at-p "[ \t]*#\\+")))
(not (denote-capf--in-link-p)))
(when-let* ((bounds (bounds-of-thing-at-point 'word)))
(when (>= (- (cdr bounds) (car bounds)) denote-capf-min-prefix)
(denote-capf--ensure)
(list (car bounds) (cdr bounds) denote-capf--titles
:exclusive 'no
:company-kind (lambda (_) 'reference)
:annotation-function (lambda (_) " ✦ note")
:exit-function #'denote-capf--link-exit)))))
(defun denote-capf-keywords ()
"Completion-at-point function for denote keywords on #+filetags lines."
(when (and (derived-mode-p 'org-mode)
(save-excursion (beginning-of-line)
(looking-at-p "[ \t]*#\\+filetags:")))
(let ((end (point))
(start (save-excursion (skip-chars-backward "A-Za-z0-9_-") (point))))
(when (< start end)
(list start end (denote-keywords)
:exclusive 'no
:company-kind (lambda (_) 'keyword)
:annotation-function (lambda (_) " ✦ keyword"))))))
(defun denote-capf--enabled-here-p ()
"Return non-nil when denote completion should run in this buffer.
Honors `denote-capf-directories'."
(or (null denote-capf-directories)
(and buffer-file-name
(seq-some (lambda (dir) (file-in-directory-p buffer-file-name dir))
denote-capf-directories))))
(defun denote-capf--add-capfs ()
"Add denote capfs to `completion-at-point-functions' locally.
Does nothing if they are already present."
(unless (memq #'denote-capf-keywords completion-at-point-functions)
(add-hook 'completion-at-point-functions #'denote-capf-keywords nil t))
(unless (memq #'denote-capf-link completion-at-point-functions)
(add-hook 'completion-at-point-functions #'denote-capf-link nil t)))
;;;###autoload
(defun denote-capf-setup ()
"Set up denote completion-at-point in the current buffer.
When called interactively, print a status message. The setup is
only performed when `denote-capf--enabled-here-p' returns non-nil."
(interactive)
(if (denote-capf--enabled-here-p)
(progn
(setq-local corfu-auto nil)
(denote-capf--add-capfs)
(unless denote-capf--title->id (denote-capf--schedule))
(when (called-interactively-p 'any)
(message "Denote completion-at-point enabled in this buffer")))
(when (called-interactively-p 'any)
(message "Denote completion-at-point is not enabled here"))))
;;;###autoload
(defun denote-capf-teardown ()
"Remove denote completion-at-point from the current buffer.
When called interactively, print a status message."
(interactive)
(remove-hook 'completion-at-point-functions #'denote-capf-keywords t)
(remove-hook 'completion-at-point-functions #'denote-capf-link t)
(when (called-interactively-p 'any)
(message "Denote completion-at-point disabled in this buffer")))
(provide 'denote-capf)
;;; denote-capf.el ends here
|