diff options
| author | cashmere <cashmere@autistici.org> | 2026-08-22 22:11:13 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-08-22 22:11:13 +0200 |
| commit | cf5d93ad6ba5c2d1e1416b6a55959cff0b2ff2c6 (patch) | |
| tree | 1dd3fa9d2e16be82129b14000d0313b8b3926229 /lisp | |
| parent | 47f033411523aee9eacc018529a0e0291036a90a (diff) | |
| download | emacs-cf5d93ad6ba5c2d1e1416b6a55959cff0b2ff2c6.tar.gz | |
add denote-capf
Diffstat (limited to 'lisp')
| -rw-r--r-- | lisp/denote-capf.el | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/lisp/denote-capf.el b/lisp/denote-capf.el new file mode 100644 index 0000000..c367635 --- /dev/null +++ b/lisp/denote-capf.el @@ -0,0 +1,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 |
