From bd49a91980869a6acea1ce9773f18e31a9799e43 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 12 Dec 2021 08:04:42 -0700 Subject: [PATCH] Add :filter argument to `citar-select-ref` (#482) * Add :filter argument to `citar-select-ref` Also simplify and update documentation for functions in `citar-file.el` Closes #272. * citar-file--make-file-predicate: Cache results of parsing file field * citar.el: Add utility functions for making file and note predicates * Update docstrings --- citar-file.el | 45 ++++++++++++++-------- citar.el | 105 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 111 insertions(+), 39 deletions(-) diff --git a/citar-file.el b/citar-file.el index 4532eb78..ad779cfb 100644 --- a/citar-file.el +++ b/citar-file.el @@ -206,22 +206,37 @@ need to scan the contents of DIRS in this case." (puthash key (nreverse filelist) files)) files)))) -(defun citar-file--has-file-p (dirs extensions &optional additional-sep entry-field) +(defun citar-file--has-file (dirs extensions &optional entry-field) "Return predicate testing whether a key and entry have associated files. Files are found in two ways: -- In DIRS using `citar-file--directory-files`; see its -documentation for the meaning of EXTENSIONS and ADDITIONAL-SEP. +- By scanning DIRS for files with EXTENSIONS using + `citar-file--directory-files`, which see. Its ADDITIONAL-SEP + argument is taken from `citar-file-additional-files-separator`. -- In the entry field ENTRY-FIELD, when it is non-nil." - (let ((files (citar-file--directory-files dirs nil extensions additional-sep))) +- When ENTRY-FIELD is non-nil, by parsing the entry field it + names using `citar-file--parse-file-field`; see its + documentation. DIRS is used to resolve relative paths and + non-existent files are ignored. + +Note: for performance reasons, this function should be called +once per command; the function it returns can be called +repeatedly." + (let ((files (citar-file--directory-files dirs nil extensions + citar-file-additional-files-separator))) (lambda (key entry) - (or (car (gethash key files)) - (when entry-field - (seq-some - #'file-exists-p - (citar-file--parse-file-field entry entry-field dirs))))))) + (let ((cached (gethash key files 'unknown))) + (if (not (eq cached 'unknown)) + cached + ;; KEY has no files in DIRS, so check the ENTRY-FIELD field of + ;; ENTRY. This will run at most once for each KEY; after that, KEY + ;; in hash table FILES will either contain nil or a file name found + ;; in ENTRY. + (puthash key (seq-some + #'file-exists-p + (citar-file--parse-file-field entry entry-field dirs)) + files)))))) (defun citar-file--files-for-entry (key entry dirs extensions) "Find files related to bibliography item KEY with metadata ENTRY. @@ -235,16 +250,16 @@ EXTENSIONS, and how files are found." KEYS-ENTRIES is a list of (KEY . ENTRY) pairs. Return a list of files found in two ways: -- Scan directories in DIRS for files starting with keys in +- By scanning directories in DIRS for files starting with keys in KEYS-ENTRIES and having extensions in EXTENSIONS. The files may also have additional text after the key, separated by the value of `citar-file-additional-files-separator`. The scanning is performed by `citar-file--directory-files`, which see. -- Parse the entries in KEYS-ENTRIES and find file names listed in - the field named by `citar-file-variable`. Relative paths are - resolved in the directories in DIRS, and only existing files - are returned." +- By parsing the field named by `citar-file-variable` of the + entries in KEYS-ENTRIES. DIRS is used to resolve relative + paths and non-existent files are ignored; see + `citar-file--parse-file-field`." (let* ((keys (seq-map #'car keys-entries)) (files (citar-file--directory-files dirs keys extensions citar-file-additional-files-separator))) diff --git a/citar.el b/citar.el index 6b0c3c10..78dfff07 100644 --- a/citar.el +++ b/citar.el @@ -316,7 +316,33 @@ of all citations in the current buffer." ;;; Completion functions -(cl-defun citar-select-ref (&optional &key rebuild-cache multiple) +(defun citar--completion-table (candidates &optional filter) + "Return a completion table for CANDIDATES. + +CANDIDATES is an alist with entries (CAND KEY . ENTRY), where + CAND is a display string for the bibliography item given + by (KEY . ENTRY). + +FILTER, if non-nil, should be a predicate function taking + arguments KEY and ENTRY. Only candidates for which this + function returns non-nil will be offered for completion. + +The returned completion table can be used with `completing-read` +and other completion functions." + (let ((metadata `(metadata (category . citar-reference) + (affixation-function . ,#'citar--affixation)))) + (lambda (string predicate action) + (if (eq action 'metadata) + metadata + (let ((predicate + (when (or filter predicate) + (lambda (cand-key-entry) + (pcase-let ((`(,cand ,key . ,entry) cand-key-entry)) + (and (or (null filter) (funcall filter key entry)) + (or (null predicate) (funcall predicate cand)))))))) + (complete-with-action action candidates string predicate)))))) + +(cl-defun citar-select-ref (&optional &key rebuild-cache multiple filter) "Select bibliographic references. A wrapper around 'completing-read' that returns (KEY . ENTRY), @@ -324,20 +350,29 @@ where ENTRY is a field-value alist. Therefore 'car' of the return value is the cite key, and 'cdr' is an alist of structured data. -Includes the following optional arguments: +Takes the following optional keyword arguments: -REBUILD-CACHE if t, forces rebuilding the cache before offering -the selection candidates. +REBUILD-CACHE: if t, forces rebuilding the cache before offering + the selection candidates. -MULTIPLE if t, calls `completing-read-multiple` and returns an -alist of (KEY . ENTRY) pairs." +MULTIPLE: if t, calls `completing-read-multiple` and returns an + alist of (KEY . ENTRY) pairs. + +FILTER: if non-nil, should be a predicate function taking + arguments KEY and ENTRY. Only candidates for which this + function returns non-nil will be offered for completion. For + example: + + (citar-select-ref :filter (citar-has-file)) + + (citar-select-ref :filter (citar-has-note)) + + (citar-select-ref + :filter (lambda (_key entry) + (when-let ((keywords (assoc-default \"keywords\" entry))) + (string-match-p \"foo\" keywords))))" (let* ((candidates (citar--get-candidates rebuild-cache)) - (metadata `(metadata (category . citar-reference) - (affixation-function . ,#'citar--affixation))) - (completions (lambda (string predicate action) - (if (eq action 'metadata) - metadata - (complete-with-action action candidates string predicate)))) + (completions (citar--completion-table candidates filter)) (embark-transformer-alist (citar--embark-transformer-alist candidates)) (crm-separator "\\s-*&\\s-*") (chosen (if multiple @@ -367,13 +402,13 @@ alist of (KEY . ENTRY) pairs." (message "Keys not found: %s" (mapconcat #'identity notfound "; "))) (if multiple keyentries (car keyentries)))) -(cl-defun citar-select-refs (&optional &key rebuild-cache) +(cl-defun citar-select-refs (&optional &key rebuild-cache filter) "Select bibliographic references. -A wrapper around 'citar-select-ref' that returns an alist of (KEY . ENTRY) -cons pairs. If REBUILD-CACHE is non-nil, forces rebuilding the cache before -offering the selection candidates." - (citar-select-ref :rebuild-cache rebuild-cache :multiple t)) +Call 'citar-select-ref' with argument :multiple; see its +documentation for the return value and the meaning of +REBUILD-CACHE and FILTER." + (citar-select-ref :rebuild-cache rebuild-cache :multiple t :filter filter)) (defun citar-select-files (files) "Select file(s) from a list of FILES." @@ -489,6 +524,33 @@ personal names of the form 'family, given'." (list citar-file-variable) citar-additional-fields)) +(defun citar-has-file () + "Return predicate testing whether entry has associated files. + +Return a function that takes arguments KEY and ENTRY and returns +non-nil when the entry has associated files, either in +`citar-library-paths` or the field named in +`citar-file-variable`. + +Note: for performance reasons, this function should be called +once per command; the function it returns can be called +repeatedly." + (citar-file--make-file-predicate citar-library-paths + citar-file-extensions + citar-file-variable)) + +(defun citar-has-note () + "Return predicate testing whether entry has associated notes. + +Return a function that takes arguments KEY and ENTRY and returns +non-nil when the entry has associated notes in `citar-notes-paths`. + +Note: for performance reasons, this function should be called +once per command; the function it returns can be called +repeatedly." + (citar-file--make-file-predicate citar-notes-paths + citar-file-note-extensions)) + (defun citar--format-candidates (bib-files &optional context) "Format candidates from BIB-FILES, with optional hidden CONTEXT metadata. This both propertizes the candidates for display, and grabs the @@ -496,13 +558,8 @@ key associated with each one." (let* ((candidates nil) (raw-candidates (parsebib-parse bib-files :fields (citar--fields-to-parse))) - (hasfilep (citar-file--has-file-p citar-library-paths - citar-file-extensions - citar-file-additional-files-separator - citar-file-variable)) - (hasnotep (citar-file--has-file-p citar-notes-paths - citar-file-note-extensions - citar-file-additional-files-separator)) + (hasfilep (citar-has-file)) + (hasnotep (citar-has-note)) (main-width (citar--format-width (citar-get-template 'main))) (suffix-width (citar--format-width (citar-get-template 'suffix))) (symbols-width (string-width (citar--symbols-string t t t)))