From fee49a8b6c0a690a5a02ebb08f0ac7f75f171380 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sat, 18 Jun 2022 20:35:55 -0600 Subject: [PATCH 01/78] Small tweaks to the Eldev and GitHub CI configuration Add advice in Eldev file to disable the `with-eval-after-load` warning in package-lint. If it produces any other warnings, however, then fail the CI check. --- .dir-locals.el | 20 +++++++++++--------- .github/workflows/check.yml | 2 +- Eldev | 7 +++++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.dir-locals.el b/.dir-locals.el index 57444ef8..729ba419 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,9 +1,11 @@ -((emacs-lisp-mode - (fill-column . 110) - (indent-tabs-mode . nil) - (elisp-lint-indent-specs . ((describe . 1) - (it . 1) - (thread-first . 0) - (cl-flet . 1) - (sentence-end-double-space . nil) - (cl-flet* . 1))))) +;;; Directory Local Variables +;;; For more information see (info "(emacs) Directory Variables") + +((emacs-lisp-mode . ((sentence-end-double-space . nil) + (fill-column . 110) + (indent-tabs-mode . nil) + (elisp-lint-indent-specs . ((describe . 1) + (it . 1) + (thread-first . 0) + (cl-flet . 1) + (cl-flet* . 1)))))) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7c689ce1..4aaf39ec 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -64,4 +64,4 @@ jobs: - name: Lint package metadata if: ${{ matrix.action == 'lint' }} run: | - eldev --color lint package || true + eldev --color lint package diff --git a/Eldev b/Eldev index 70f206ce..2e08d30b 100755 --- a/Eldev +++ b/Eldev @@ -16,8 +16,7 @@ ;; allow to load test helpers ;; (eldev-add-loading-roots 'test "test/utils") -;; Tell checkdoc not to demand two spaces after a period. -(setq sentence-end-double-space nil) +;;; Linting settings (setf eldev-lint-default '(elisp)) @@ -28,3 +27,7 @@ ;; Emacs 29 snapshot has new indentation convention for cl-letf (when (> emacs-major-version 28) (push "indent" elisp-lint-ignored-validators))) + +;; Currently, package-lint has no other way of ignoring checks. +;; See https://github.com/purcell/package-lint/issues/125 +(advice-add #'package-lint--check-eval-after-load :override #'ignore) From bdbd046a3d1c9d7506734dcbd1a16ceff8d03b5b Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 7 Jun 2022 10:27:51 -0400 Subject: [PATCH 02/78] refactor: split caches, make API key-focused Sorry for this breaking change, but I wanted to get the foundations right before tagging 1.0. This completely restructures the core of citar to borrow some code and ideas from the org-mode oc-basic package. In particular, it changes to using two primary caches: - bibliography - completion Both of these now use hash tables, rather than lists. Caching functionality is also changed, and the API now focuses on citekeys as arguments for key functions. Finally, citar--parse-bibliography should re-parse bibliography files upon change. Fix #623 Close #627 --- CONTRIBUTING.org | 45 ++-- citar-file.el | 37 +-- citar.el | 572 +++++++++++++++++++++-------------------------- 3 files changed, 298 insertions(+), 356 deletions(-) diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org index 91e140c7..df68cb37 100644 --- a/CONTRIBUTING.org +++ b/CONTRIBUTING.org @@ -5,31 +5,49 @@ If you would like to contribute, details: -- For more signifiant potential changes, file an issue first to get feedback on the basic idea. +- For more significant potential changes, file an issue first to get feedback on the basic idea. - If you do submit a PR, follow the [[https://github.com/bbatsov/emacs-lisp-style-guide][elisp style guide]], and [[https://cbea.ms/git-commit/][these suggestions]] on git commit messages. - For working on lists and such, we primarily use the =seq= functions, and occassionally ~dolist~. +** Basic Architecture + +Citar has two primary caches, each of which store the data in hash tables: + +- bibliographic :: keys are citekeys, values are alists of entry fields +- completion :: keys are completion strings, values are citekeys + +The =citar--ref-completion-table= function returns a hash table from the bibliographic cache, and ~citar--get-entry~ and ~-citar--get-value~ provide access to those data. +Most user-accessible citar functions take an argument ~key~ or ~keys~. +Some functions also take an ~entry~ argument, and ~citar--get-value~ takes either. +When using these functions, you should keep in mind that unless you pass an entry alist to ~citar--get-value~, and instead use a key, each call to that function will query the cache. +This, therefore, is a better pattern to use: + +#+begin_src emacs-lisp + +(let* ((entry (citar--get-entry key)) + (title (citar--get-value entry "title"))) + (message title)) + +#+end_src + + ** Extending citar -Most user-accessible citar functions take an argument ~key-entry~ or ~keys-entries~. -These expect, respectively, a cons cell of a citation key (a string like "SmithWritingHistory1987") and the corresponding bibliography entry for that citation, or a list of such cons cells. -If you wish to extend citar at the user-action level, perhaps by adding a function to one of the embark keymaps, you will find it easiest to reproduce this pattern. -If you need to build the cons cells manually, this can be accomplished via ~citar--get-entry~. -So, for example, to insert the annotations from a pdf into a buffer, the following pair of functions might be used: +You can use ~citar-select-ref~ or ~citar-select-refs~ to write custom commands. +An example: #+begin_src emacs-lisp -(defun my/citar-insert-annots (keys-entries) +(defun my/citar-insert-annots (keys) "insert annotations as org text from KEYS-ENTRIES" - (interactive (list (citar-select-refs - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-refs))) (let* ((files - (seq-mapcat (lambda (key-entry) + (seq-mapcat (lambda (key) (citar-file--files-for-entry - (car key-entry) (cdr key-entry) + key (citar--get-entry key) '("/") '("pdf"))) - keys-entries )) + keys )) (output (seq-map (lambda (file) (pdf-annot-markups-as-org-text ;; you'll still need to write this function! @@ -44,8 +62,7 @@ So, for example, to insert the annotations from a pdf into a buffer, the followi (defun my/independent-insert-annots (key) "helper function to insert annotations without the bibtex-actins apparatus" - (let ((key-entry (cons key (citar--get-entry key)))) - (my/citar-insert-annots (list key-entry)))) + (my/citar-insert-annots (list key))) #+end_src diff --git a/citar-file.el b/citar-file.el index 9c8e88fc..75e0dc3f 100644 --- a/citar-file.el +++ b/citar-file.el @@ -233,31 +233,6 @@ need to scan the contents of DIRS in this case." (puthash key (nreverse filelist) files)) files)))) -(defun citar-file--has-file-notes-hash () - "Return a hash of keys and file paths for notes." - (citar-file--directory-files - citar-notes-paths nil citar-file-note-extensions - citar-file-additional-files-separator)) - -(defun citar-file--has-library-files-hash () - "Return a hash of keys and file paths for library files." - (citar-file--directory-files - citar-library-paths nil citar-library-file-extensions - citar-file-additional-files-separator)) - -(defun citar-file--keys-with-file-notes () - "Return a list of keys with file notes." - (hash-table-keys (citar-file--has-file-notes-hash))) - -(defun citar-file--keys-with-library-files () - "Return a list of keys with file notes." - (hash-table-keys (citar-file--has-library-files-hash))) - -(defun citar-file-has-notes () - "Return a predicate testing whether a reference has associated notes." - (citar-file--has-file citar-notes-paths - citar-file-note-extensions)) - (defun citar-file--has-file (dirs extensions &optional entry-field) "Return predicate testing whether a key and entry have associated files. @@ -277,8 +252,9 @@ 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) - (let* ((xref (citar--get-value "crossref" entry)) + (lambda (key &optional entry) + (let* ((nentry (or entry (citar--get-entry key))) + (xref (citar--get-value "crossref" nentry)) (cached (if (and xref (not (eq 'unknown (gethash xref files 'unknown)))) (gethash xref files 'unknown) @@ -292,7 +268,7 @@ repeatedly." (puthash key (seq-some #'file-exists-p - (citar-file--parse-file-field entry entry-field dirs extensions)) + (citar-file--parse-file-field nentry entry-field dirs extensions)) files)))))) (defun citar-file--files-for-entry (key entry dirs extensions) @@ -351,6 +327,11 @@ of files found in two ways: nil 0 nil file))) +(defun citar-file-has-notes () + "Return a predicate testing whether a reference has associated notes." + (citar-file--has-file citar-notes-paths + citar-file-note-extensions)) + (defun citar-file--open-note (key entry) "Open a note file from KEY and ENTRY." (if-let* ((file (citar-file--get-note-filename key diff --git a/citar.el b/citar.el index 8260ae39..572c4bb8 100644 --- a/citar.el +++ b/citar.el @@ -237,15 +237,17 @@ If nil, single resources will open without prompting." (defcustom citar-open-note-functions '(citar-file--open-note) "List of functions to open a note." + ;; REVIEW change to key only arg? :group 'citar :type '(function)) (defcustom citar-has-note-functions - '(citar-file-has-notes) + '(citar--has-file-notes) "Functions used for displaying note indicators. -Such functions must take arguments KEY and ENTRY and return -non-nil when the reference has associated notes." +Such functions must take KEY and return non-nil when the +reference has associated notes." + ;; REVIEW change to key only arg? :group 'citar :type '(function)) @@ -257,6 +259,7 @@ A note function must take two arguments: KEY: a string to represent the citekey ENTRY: an alist with the structured data (title, author, etc.)" + ;; REVIEW change to key only arg? :group 'citar :type 'function) @@ -269,6 +272,7 @@ A note function must take three arguments: KEY: a string to represent the citekey ENTRY: an alist with the structured data (title, author, etc.) FILEPATH: the file name." + ;; REVIEW change to key only arg? :group 'citar :type 'function) @@ -349,6 +353,12 @@ of all citations in the current buffer." :group 'citar :type '(repeat string)) +(defcustom citar-select-multiple t + "Use `completing-read-multiple' for selecting citation keys. +When nil, all citar commands will use `completing-read`." + :type 'boolean + :group 'citar) + ;;; Keymaps (defvar citar-map @@ -380,24 +390,34 @@ of all citations in the current buffer." map) "Keymap for Embark citation-key actions.") -;;; Completion functions +;; Internal variables -(defcustom citar-select-multiple t - "Use `completing-read-multiple' for selecting citation keys. -When nil, all citar commands will use `completing-read`." - :type 'boolean - :group 'citar) +;; Most of this design is adapted from org-mode 'oc-basic', +;; written by Nicolas Goaziou. + +(defvar citar--bibliography-cache nil + "Cache for parsed bibliography files. +This is an association list following the pattern: + (FILE-ID . ENTRIES) +FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of +the bibliography file, and HASH a hash of its contents. +ENTRIES is a hash table with citation references as keys and fields alist as +values.") + +(defvar citar--completion-cache (make-hash-table :test #'equal) + "Hash with key as completion string, value as citekey.") + +;;; Completion functions (defun citar--completion-table (candidates &optional filter &rest metadata) "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). +CANDIDATES is a hash with references CAND as key and CITEKEY as value, + where CAND is a display string for the bibliography item. 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. + argument KEY. Only candidates for which this function returns + non-nil will be offered for completion. By default the metadata of the table contains the category and affixation function. METADATA are extra entries for metadata of @@ -406,20 +426,22 @@ the form (KEY . VAL). The returned completion table can be used with `completing-read` and other completion functions." (let ((metadata `(metadata . ((category . citar-reference) - . ((affixation-function . ,#'citar--affixation) + . ((affixation-function . ,#'citar--ref-affix) . ,metadata))))) (lambda (string predicate action) (if (eq action 'metadata) metadata + ;; REVIEW this now works, but probably needs refinement (let ((predicate (when (or filter predicate) - (lambda (cand-key-entry) - (pcase-let ((`(,cand ,key . ,entry) cand-key-entry)) + (lambda (cand _) + (let* ((key (gethash cand candidates)) + (entry (citar--get-entry key))) (and (or (null filter) (funcall filter key entry)) - (or (null predicate) (funcall predicate cand)))))))) + (or (null predicate) (funcall predicate string)))))))) (complete-with-action action candidates string predicate)))))) -(cl-defun citar-select-ref (&optional &key rebuild-cache multiple filter) +(cl-defun citar-select-ref (&optional &key multiple filter) "Select bibliographic references. A wrapper around `completing-read' that returns (KEY . ENTRY), @@ -429,9 +451,6 @@ data. Takes the following optional keyword arguments: -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. @@ -440,49 +459,32 @@ FILTER: if non-nil, should be a predicate function taking 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 'citar-has-library-file-p) - (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)) + (citar-select-ref :filter 'citar-has-note-p)" + ;; TODO readd an example filter or two above? + (let* ((candidates (or (citar--ref-completion-table) + (user-error "No bibliography set"))) (chosen (if (and multiple citar-select-multiple) (citar--select-multiple "References: " candidates filter 'citar-history citar-presets) (completing-read "Reference: " (citar--completion-table candidates filter) - nil nil nil 'citar-history citar-presets nil))) - (notfound nil) - (keyentries - (seq-mapcat - ;; Find citation key-entry of selected candidate. - ;; CHOICE is either the formatted candidate string, or the citation - ;; key when called through `embark-act`. To handle both cases, test - ;; CHOICE against the first two elements of the entries of - ;; CANDIDATES. See - ;; https://github.com/bdarcus/citar/issues/233#issuecomment-901536901 - (lambda (choice) - (if-let ((cand (seq-find - (lambda (cand) (member choice (seq-take cand 2))) - candidates))) - (list (cdr cand)) - ;; If not found, add CHOICE to NOTFOUND and return nil - (push choice notfound) - nil)) - (if (listp chosen) chosen (list chosen))))) - (when notfound - (message "Keys not found: %s" (mapconcat #'identity notfound "; "))) - (if multiple keyentries (car keyentries)))) - -(cl-defun citar-select-refs (&optional &key rebuild-cache filter) + nil nil nil 'citar-history citar-presets nil)))) + ;; Return a list of keys regardless of 1 or many + (if (stringp chosen) + (list (gethash chosen candidates)) + (seq-map + (lambda (choice) + (gethash choice candidates)) + chosen)))) + +(cl-defun citar-select-refs (&optional &key filter) "Select bibliographic references. 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)) + (citar-select-ref :multiple t :filter filter)) (defun citar--multiple-completion-table (selected-hash candidates filter) "Return a completion table for multiple selection. @@ -530,7 +532,7 @@ HISTORY is the `completing-read' history argument." (completing-read (format "%s (%s/%s): " prompt (hash-table-count selected-hash) - (length candidates)) + (hash-table-count candidates)) (citar--multiple-completion-table selected-hash candidates filter) nil t nil history `("" . ,def))))) (unless (equal item "") @@ -576,6 +578,88 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) +(defun citar--ref-completion-table () + "Return completion table for cite keys, as a hash table. +In this hash table, keys are a strings with author, date, and +title of the reference. Values are the cite keys. +Return nil if there are no bibliography files or no entries." + ;; Populate bibliography cache. + (let* ((entries (citar--parse-bibliography)) + (hasnotep (citar-has-note)) + (hasfilep (citar-has-file)) + (mainwidth (citar--format-width (citar--get-template 'main))) + (suffixwidth (citar--format-width (citar--get-template 'suffix))) + (symbolswidth (string-width (citar--symbols-string t t t))) + (starwidth + (- (frame-width) (+ 2 symbolswidth mainwidth suffixwidth)))) + (cond + ((null entries) nil) ; no bibliography files + ;; if completion-cache is same as bibliography-cache, use the former + ((gethash entries citar--completion-cache) + citar--completion-cache) ; REVIEW ? + (t + (clrhash citar--completion-cache) + (dolist (key (citar--all-keys)) + (let* ((entry (citar--get-entry key)) + (hasfile + (when (funcall hasfilep key entry) "has:file")) + (hasnote + (when (funcall hasnotep key entry) "has:note")) + (candidatemain + (citar--format-entry + entry + starwidth + (citar--get-template 'main))) + (candidatesuffix + (citar--format-entry + entry + starwidth + (citar--get-template 'suffix))) + (invisible (concat hasfile " " hasnote)) + (completion + (string-trim-right + (concat + (propertize candidatemain 'face 'citar-highlight) " " + (propertize candidatesuffix 'face 'citar) + (propertize invisible 'invisible t))))) + (puthash completion key citar--completion-cache))) + (unless (map-empty-p citar--completion-cache) ; no key + (puthash entries t citar--completion-cache) ; REVIEW ? + citar--completion-cache))))) + +;; adapted from 'org-cite-basic--parse-bibliography' +(defvar citar--file-id-cache nil + "Hash table linking files to their hash.") + +(defun citar--parse-bibliography () + "List all entries available in the buffer. +Each association follows the pattern + (FILE . ENTRIES) +where FILE is the absolute file name of the bibliography file, +and ENTRIES is a hash table where keys are references and values +are association lists between fields, as symbols, and values as +strings or nil." + (unless (hash-table-p citar--file-id-cache) + (setq citar--file-id-cache (make-hash-table :test #'equal))) + (let ((results nil)) + ;; FIX the files to parse needs to be a function that returns the right + ;; local and/or global bibliography files for the current buffer. + (dolist (file citar-bibliography) + (when (file-readable-p file) + (with-temp-buffer + (when (or (file-has-changed-p file) + (not (gethash file citar--file-id-cache))) + (insert-file-contents file) + (puthash file (md5 (current-buffer)) citar--file-id-cache)) + (let* ((file-id (cons file (gethash file citar--file-id-cache))) + (entries + (or (cdr (assoc file-id citar--bibliography-cache)) + (let ((table (parsebib-parse file))) + (push (cons file-id table) citar--bibliography-cache) + table)))) + (push (cons file entries) results))))) + results)) + (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. If no function is found matching KEY for the current major mode, @@ -603,9 +687,36 @@ If no function is found, the DEFAULT function is called." (citar-file--normalize-paths citar-bibliography))) -(defun citar--get-value (field entry) - "Return the FIELD value for ENTRY." - (cdr (assoc-string field entry 'case-fold))) +;; Data access functions + +(cl-defun citar-get-data-entries (&optional &key filter) + "Return a subset of entries in bibliography by FILTER. + + (citar-get-data-entries :filter (citar-has-note))" + (let ((results (make-hash-table :test #'equal))) + (dolist (bibliography citar--bibliography-cache) + (maphash + (lambda (citekey entry) + (when (funcall filter citekey) + (puthash citekey entry results))) + (cdr bibliography))) + results)) + +(defun citar--get-entry (key) + "Return entry for KEY, as an association list." + (catch :found + ;; Iterate through the cached bibliography hashes and find a key. + (pcase-dolist (`(,_ . ,entries) (citar--parse-bibliography)) + (let ((entry (gethash key entries))) + (when entry (throw :found entry)))) + nil)) + +(defun citar--get-value (field key-or-entry) + "Return FIELD value for KEY-OR-ENTRY." + (let ((entry (if (stringp key-or-entry) + (citar--get-entry key-or-entry) + key-or-entry))) + (cdr (assoc-string field entry)))) (defun citar--field-with-value (fields entry) "Return the first field that has a value in ENTRY among FIELDS ." @@ -694,74 +805,32 @@ repeatedly." ;; Call each function in `citar-has-note-functions` to get a list of predicates (let ((preds (mapcar #'funcall citar-has-note-functions))) ;; Return a predicate that checks if `citekey` and `entry` have a note - (lambda (citekey entry) - ;; Call each predicate with `citekey` and `entry`; return the first non-nil result - (seq-some (lambda (pred) (funcall pred citekey entry)) preds)))) - -(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 -key associated with each one." - (let* ((candidates nil) - (raw-candidates - (parsebib-parse bib-files :fields (citar--fields-to-parse))) - (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))) - (star-width (- (frame-width) (+ 2 symbols-width main-width suffix-width)))) - (maphash - (lambda (citekey entry) - (let* ((files (when (funcall hasfilep citekey entry) " has:files")) - (notes (when (funcall hasnotep citekey entry) " has:notes")) - (link (when (citar--field-with-value '("doi" "url") entry) "has:link")) - (candidate-main - (citar--format-entry - entry - star-width - (citar--get-template 'main))) - (candidate-suffix - (citar--format-entry - entry - star-width - (citar--get-template 'suffix))) - ;; We display this content already using symbols; here we add back - ;; text to allow it to be searched, and citekey to ensure uniqueness - ;; of the candidate. - (candidate-hidden (string-join (list files notes link context citekey) " "))) - (when files (push (cons "has-file" t) entry)) - (when notes (push (cons "has-note" t) entry)) - (push - (cons - ;; If we don't trim the trailing whitespace, - ;; 'completing-read-multiple' will get confused when there are - ;; multiple selected candidates. - (string-trim-right - (concat - ;; We need all of these searchable: - ;; 1. the 'candidate-main' variable to be displayed - ;; 2. the 'candidate-suffix' variable to be displayed with a different face - ;; 3. the 'candidate-hidden' variable to be hidden - (propertize candidate-main 'face 'citar-highlight) " " - (propertize candidate-suffix 'face 'citar) " " - (propertize candidate-hidden 'invisible t))) - (cons citekey entry)) - candidates))) - raw-candidates) - candidates)) - -(defun citar--affixation (cands) + (lambda (citekey &optional entry) + (let ((nentry (or entry (citar--get-entry citekey)))) + ;; Call each predicate with `citekey` and `entry`; return the first non-nil result + (seq-some (lambda (pred) (funcall pred citekey nentry)) preds))))) + +(defun citar--ref-affix (cands) "Add affixation prefix to CANDS." (seq-map (lambda (candidate) - (let ((candidate-symbols (citar--symbols-string - (string-match "has:files" candidate) - (string-match "has:notes" candidate) - (string-match "has:link" candidate)))) - (list candidate candidate-symbols ""))) + (let ((symbols (citar--ref-make-symbols candidate))) + (list candidate symbols ""))) cands)) +(defun citar--ref-make-symbols (cand) + "Make CAND annotation or affixation string for has-symbols." + (let ((candidate-symbols (citar--symbols-string + (string-match "has:file" cand) + (string-match "has:note" cand) + (string-match "has:link" cand)))) + candidate-symbols)) + +(defun citar--ref-annotate (cand) + "Add annotation to CAND." + ;; REVIEW/TODO we don't currently use this, but could, for Emacs 27. + (citar--ref-make-symbols cand)) + (defun citar--symbols-string (has-files has-note has-link) "String for display from booleans HAS-FILES HAS-LINK HAS-NOTE." (cl-flet ((thing-string (has-thing thing-symbol) @@ -782,38 +851,6 @@ key associated with each one." "") ""))) -(defvar citar--candidates-cache 'uninitialized - "Store the global candidates list. - -Default value of `uninitialized' is used to indicate that cache -has not yet been created.") - -(defvar-local citar--local-candidates-cache 'uninitialized - ;; We use defvar-local so can maintain per-buffer candidate caches. - "Store the local (per-buffer) candidates list.") - -;;;###autoload -(defun citar-refresh (&optional force-rebuild-cache scope) - "Reload the candidates cache. - -If called interactively with a prefix or if FORCE-REBUILD-CACHE -is non-nil, also run the `citar-before-refresh-hook' hook. - -If SCOPE is `global' only global cache is refreshed, if it is -`local' only local cache is refreshed. With any other value both -are refreshed." - (interactive (list current-prefix-arg nil)) - (when force-rebuild-cache - (run-hooks 'citar-force-refresh-hook)) - (unless (eq 'local scope) - (setq citar--candidates-cache - (citar--format-candidates - (citar-file--normalize-paths citar-bibliography)))) - (unless (eq 'global scope) - (setq citar--local-candidates-cache - (citar--format-candidates - (citar--local-files-to-cache) "is:local")))) - (defun citar--get-template (template-name) "Return template string for TEMPLATE-NAME." (let ((template @@ -822,40 +859,11 @@ are refreshed." (error "No template for \"%s\" - check variable 'citar-templates'" template-name)) template)) -(defun citar--get-candidates (&optional force-rebuild-cache filter) - "Get the cached candidates. - -If the cache is unintialized, this will load the cache. - -If FORCE-REBUILD-CACHE is t, force reload the cache. - -If FILTER, use the function to filter the candidate list." - (when force-rebuild-cache - (citar-refresh force-rebuild-cache)) - (when (eq 'uninitialized citar--candidates-cache) - (citar-refresh nil 'global)) - (when (eq 'uninitialized citar--local-candidates-cache) - (citar-refresh nil 'local)) - (let ((candidates - (seq-concatenate 'list - citar--local-candidates-cache - citar--candidates-cache))) - (if candidates - (if filter - (seq-filter - (pcase-lambda (`(_ ,citekey . ,entry)) - (funcall filter citekey entry)) - candidates) - candidates) - (unless (or citar--candidates-cache citar--local-candidates-cache) - (error "Make sure to set citar-bibliography and related paths")) ))) - -(defun citar--get-entry (key) - "Return the cached entry for KEY." - (cddr (seq-find - (lambda (entry) - (string-equal key (cadr entry))) - (citar--get-candidates)))) +(defun citar--all-keys () + "List all keys available in current bibliography." + (seq-mapcat (pcase-lambda (`(,_ . ,entries)) + (map-keys entries)) + (citar--parse-bibliography))) (defun citar--get-link (entry) "Return a link for an ENTRY." @@ -867,62 +875,7 @@ If FILTER, use the function to filter the candidate list." (when field (concat base-url (citar--get-value field entry))))) -(defun citar--extract-keys (keys-entries) - "Extract list of keys from KEYS-ENTRIES. - -Each element of KEYS-ENTRIES should be either a (KEY . ENTRY) -pair or a string KEYS. - -- If it is a (KEY . ENTRY) pair, it is replaced by KEY in the - returned list. - -- Otherwise, it should be a string KEYS consisting of multiple - keys separated by \" & \". The string is split and the - separated keys are included in the returned list. - -Return a list containing only KEY strings." - (seq-mapcat - (lambda (key-entry) - (if (consp key-entry) - (list (car key-entry)) - (split-string key-entry " & "))) - keys-entries)) - -(defun citar--ensure-entries (keys-entries) - "Return copy of KEYS-ENTRIES with every element a (KEY . ENTRY) pair. - -Each element of KEYS-ENTRIES should be either a (KEY . ENTRY) -pair or a string KEYS. - -- If it is a (KEY . ENTRY) pair, it is included in the returned - list. - -- Otherwise, it should be a string KEYS consisting of multiple - keys separated by \" & \". Look up the corresponding ENTRY for - each KEY and, if found, include the (KEY . ENTRY) pairs in the - returned list. - -Return a list containing only (KEY . ENTRY) pairs." - (if (seq-every-p #'consp keys-entries) - keys-entries - ;; Get candidates only if some key has a missing entry, to avoid nasty - ;; recursion issues like https://github.com/bdarcus/citar/issues/286. Also - ;; avoids lots of memory allocation in the common case when all entries are - ;; present. - (let ((candidates (citar--get-candidates))) - (seq-mapcat - (lambda (key-entry) - (if (consp key-entry) - (list key-entry) - (seq-remove ; remove keys not found in CANDIDATES - #'null - (seq-map - (lambda (key) - (cdr (seq-find (lambda (cand-key-entry) - (string= key (cadr cand-key-entry))) - candidates))) - (split-string key-entry " & "))))) - keys-entries)))) +;; REVIEW I removed 'citar--ensure-entries' ;;;###autoload (defun citar-insert-preset () @@ -1021,6 +974,7 @@ FORMAT-STRING." "Look up key for a citar-reference TYPE and TARGET." (cons type (or (cadr (assoc target (with-current-buffer (embark--target-buffer) + ;; FIX how? (citar--get-candidates))))))) (defun citar--embark-selected () @@ -1062,11 +1016,10 @@ FORMAT-STRING." ;;; Commands ;;;###autoload -(defun citar-open (keys-entries) - "Open related resources (links or files) for KEYS-ENTRIES." +(defun citar-open (keys) + "Open related resources (links or files) for KEYS." (interactive (list - (list (citar-select-ref - :rebuild-cache current-prefix-arg)))) + (list (citar-select-ref)))) (when (and citar-library-paths (stringp citar-library-paths)) (message "Make sure 'citar-library-paths' is a list of paths")) @@ -1077,15 +1030,15 @@ FORMAT-STRING." (key-entry-alist (citar--ensure-entries keys-entries)) (files (citar-file--files-for-multiple-entries - key-entry-alist + keys (append citar-library-paths citar-notes-paths) ;; find files with any extension: nil)) (links (seq-map - (lambda (key-entry) - (citar--get-link (cdr key-entry))) - key-entry-alist)) + (lambda (key) + (citar--get-link key)) + keys)) (resource-candidates (delete-dups (append files (remq nil links))))) (cond ((eq nil resource-candidates) @@ -1107,14 +1060,12 @@ For use with `embark-act-all'." (find-file selection)) (t (citar-file-open selection)))) -(defun citar--library-file-action (key-entry action) - "Run ACTION on file associated with KEY-ENTRY." +(defun citar--library-file-action (key action) + "Run ACTION on file associated with KEY." (let* ((fn (pcase action ('open 'citar-file-open) ('attach 'mml-attach-file))) - (ke (citar--ensure-entries key-entry)) - (key (caar ke)) - (entry (cdar ke)) + (entry (citar--get-entry key)) (files (citar-file--files-for-entry key @@ -1135,25 +1086,23 @@ For use with `embark-act-all'." "Open library file associated with the KEY-ENTRY. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-ref))) (let ((embark-default-action-overrides '((file . citar-file-open)))) (when (and citar-library-paths (stringp citar-library-paths)) (error "Make sure 'citar-library-paths' is a list of paths")) - (citar--library-file-action key-entry 'open))) + (citar--library-file-action key 'open))) ;;;###autoload -(defun citar-open-notes (key-entry) - "Open notes associated with the KEY-ENTRY. +(defun citar-open-notes (key) + "Open notes associated with the KEY. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) + ;; REVIEW KEY, or KEYS + (interactive (list (citar-select-ref))) (let* ((embark-default-action-overrides '((file . find-file))) - (key (car key-entry)) - (entry (cdr key-entry))) + (entry (citar--get-entry key))) (if (listp citar-open-note-functions) - (citar--open-notes key entry) + (citar--open-notes (car key) entry) (error "Please change the value of 'citar-open-note-functions' to a list")))) (defun citar--open-notes (key entry) @@ -1164,26 +1113,23 @@ With prefix, rebuild the cache before offering candidates." (funcall citar-create-note-function key entry))) ;;;###autoload -(defun citar-open-entry (key-entry) - "Open bibliographic entry associated with the KEY-ENTRY. +(defun citar-open-entry (key) + "Open bibliographic entry associated with the KEY. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) - (when-let* ((key (car key-entry)) - (bibtex-files - (seq-concatenate - 'list - citar-bibliography - (citar--local-files-to-cache)))) - (bibtex-search-entry key t nil t))) + (interactive (list (citar-select-ref))) + (when-let ((bibtex-files + (seq-concatenate + 'list + citar-bibliography + (citar--local-files-to-cache)))) + (bibtex-search-entry (car key) t nil t))) ;;;###autoload -(defun citar-insert-bibtex (keys-entries) - "Insert bibliographic entry associated with the KEYS-ENTRIES. +(defun citar-insert-bibtex (keys) + "Insert bibliographic entry associated with the KEYS. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-refs - :rebuild-cache current-prefix-arg))) - (dolist (key (citar--extract-keys keys-entries)) + (interactive (list (citar-select-refs))) + (dolist (key keys) (citar--insert-bibtex key))) (defun citar--insert-bibtex (key) @@ -1218,20 +1164,21 @@ directory as current buffer." (citar--insert-bibtex key))))) ;;;###autoload -(defun citar-open-link (key-entry) - "Open URL or DOI link associated with the KEY-ENTRY in a browser. +(defun citar-open-link (key) + "Open URL or DOI link associated with the KEY in a browser. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref :rebuild-cache current-prefix-arg))) - (let ((link (citar--get-link (cdr key-entry)))) + (let* ((entry (citar--get-entry key)) + (link (citar--get-link entry))) (if link (browse-url link) - (message "No link found for %s" (car key-entry))))) + (message "No link found for %s" key)))) ;;;###autoload -(defun citar-insert-citation (keys-entries &optional arg) - "Insert citation for the KEYS-ENTRIES. +(defun citar-insert-citation (keys &optional arg) + "Insert citation for the KEYS. Prefix ARG is passed to the mode-specific insertion function. It should invert the default behaviour for that mode with respect to @@ -1246,7 +1193,7 @@ citation styles. See specific functions for more detail." (citar--major-mode-function 'insert-citation #'ignore - (citar--extract-keys keys-entries) + keys arg)) (defun citar-insert-edit (&optional arg) @@ -1261,57 +1208,54 @@ ARG is forwarded to the mode-specific insertion function given in arg)) ;;;###autoload -(defun citar-insert-reference (keys-entries) - "Insert formatted reference(s) associated with the KEYS-ENTRIES." +(defun citar-insert-reference (keys) + "Insert formatted reference(s) associated with the KEYS." (interactive (list (citar-select-refs))) - (let ((key-entry-alist (citar--ensure-entries keys-entries))) - (insert (funcall citar-format-reference-function key-entry-alist)))) + (insert (funcall citar-format-reference-function keys))) ;;;###autoload -(defun citar-copy-reference (keys-entries) - "Copy formatted reference(s) associated with the KEYS-ENTRIES." +(defun citar-copy-reference (keys) + "Copy formatted reference(s) associated with the KEYS." (interactive (list (citar-select-refs))) - (let* ((key-entry-alist (citar--ensure-entries keys-entries)) - (references (funcall citar-format-reference-function key-entry-alist))) + (let ((references (funcall citar-format-reference-function keys))) (if (not (equal "" references)) (progn (kill-new references) (message (format "Copied:\n%s" references))) (message "Key not found.")))) -(defun citar-format-reference (key-entry-alist) - "Return formatted reference(s) for the elements of KEY-ENTRY-ALIST." +(defun citar-format-reference (keys) + "Return formatted reference(s) for the elements of KEYS." (let* ((template (citar--get-template 'preview)) (references (with-temp-buffer - (dolist (key-entry key-entry-alist) + (dolist (key keys) (when template - (insert (citar--format-entry-no-widths (cdr key-entry) template)))) + (insert (citar--format-entry-no-widths key template)))) (buffer-string)))) references)) ;;;###autoload -(defun citar-insert-keys (keys-entries) - "Insert KEYS-ENTRIES citekeys. +(defun citar-insert-keys (keys) + "Insert KEYS citekeys. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-refs :rebuild-cache current-prefix-arg))) (citar--major-mode-function 'insert-keys #'citar--insert-keys-comma-separated - (citar--extract-keys keys-entries))) + keys)) (defun citar--insert-keys-comma-separated (keys) "Insert comma separated KEYS." (insert (string-join keys ", "))) ;;;###autoload -(defun citar-attach-library-file (key-entry) - "Attach library file associated with KEY-ENTRY to outgoing MIME message. +(defun citar-attach-library-file (key) + "Attach library file associated with KEY to outgoing MIME message. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-ref))) (let ((embark-default-action-overrides '((file . mml-attach-file)))) (when (and citar-library-paths (stringp citar-library-paths)) @@ -1350,8 +1294,8 @@ URL." (url-copy-file url (concat file-path extension) 1))))))) ;;;###autoload -(defun citar-add-file-to-library (key-entry) - "Add a file to the library for KEY-ENTRY. +(defun citar-add-file-to-library (key) + "Add a file to the library for KEY. The FILE can be added either from an open buffer, a file, or a URL." (interactive (list (citar-select-ref @@ -1359,9 +1303,9 @@ URL." (citar--add-file-to-library (car key-entry))) ;;;###autoload -(defun citar-run-default-action (keys-entries) - "Run the default action `citar-default-action' on KEYS-ENTRIES." - (funcall citar-default-action keys-entries)) +(defun citar-run-default-action (keys) + "Run the default action `citar-default-action' on KEYS." + (funcall citar-default-action keys)) ;;;###autoload (defun citar-dwim () From cc498e367aad07f2652463f8c8cc0d036d5e5b10 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 10 Jun 2022 09:49:38 -0400 Subject: [PATCH 03/78] Add citar--bibliography-files This functions returns all local and global bibliography files for 'citar--parse-bibliography' to parse. --- citar.el | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/citar.el b/citar.el index 572c4bb8..699743f3 100644 --- a/citar.el +++ b/citar.el @@ -459,10 +459,9 @@ FILTER: if non-nil, should be a predicate function taking function returns non-nil will be offered for completion. For example: - (citar-select-ref :filter 'citar-has-library-file-p) + (citar-select-ref :filter (citar-has-note)) - (citar-select-ref :filter 'citar-has-note-p)" - ;; TODO readd an example filter or two above? + (citar-select-ref :filter (citar-has-file))" (let* ((candidates (or (citar--ref-completion-table) (user-error "No bibliography set"))) (chosen (if (and multiple citar-select-multiple) @@ -578,6 +577,10 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) +(defun citar--bibliography-files () + "The list of global and local bibliography files." + (seq-concatenate 'list citar-bibliography (citar--local-files-to-cache))) + (defun citar--ref-completion-table () "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and @@ -642,9 +645,7 @@ strings or nil." (unless (hash-table-p citar--file-id-cache) (setq citar--file-id-cache (make-hash-table :test #'equal))) (let ((results nil)) - ;; FIX the files to parse needs to be a function that returns the right - ;; local and/or global bibliography files for the current buffer. - (dolist (file citar-bibliography) + (dolist (file (citar--bibliography-files)) (when (file-readable-p file) (with-temp-buffer (when (or (file-has-changed-p file) From 4960ed94ed71e40fee5ceed5f178a1974a5bcdf5 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 10 Jun 2022 11:00:29 -0400 Subject: [PATCH 04/78] Add citar-prefilter-entries Allows to independently turn off whether to do this by default, and whether to toggle the behavior. --- citar.el | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/citar.el b/citar.el index 699743f3..5d8fc29a 100644 --- a/citar.el +++ b/citar.el @@ -180,6 +180,25 @@ All functions that match a particular field are run in order." :type '(alist :key-type (choice (const t) (repeat string)) :value-type function)) +(defcustom citar-prefilter-entries '(nil . t) + "When non-nil pre-filter note and library files commands. +For commands like 'citar-open-notes', this will only show +completion candidates that have such notes. + +The downside is that, if using Embark and you want to use a different +command for the action, you will not be able to remove the +filter. + +The value should be a cons of the form: + +(FILTER . TOGGLE) + +FILTER turns this on by default + +TOGGLE use prefix arg to toggle behavior" + :group 'citar + :type 'cons) + (defcustom citar-symbols `((file . ("F" . " ")) (note . ("N" . " ")) From 59d067cdd757cad0b9a48b27ea57a69be9d19992 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 10 Jun 2022 13:42:11 -0600 Subject: [PATCH 05/78] Add implementation of cache and new formatting functions. --- citar.el | 321 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 266 insertions(+), 55 deletions(-) diff --git a/citar.el b/citar.el index 5d8fc29a..e5f8542a 100644 --- a/citar.el +++ b/citar.el @@ -409,12 +409,41 @@ When nil, all citar commands will use `completing-read`." map) "Keymap for Embark citation-key actions.") -;; Internal variables - -;; Most of this design is adapted from org-mode 'oc-basic', -;; written by Nicolas Goaziou. - -(defvar citar--bibliography-cache nil +;;; Bibliography cache + +(cl-defstruct (citar--bibliography + (:constructor citar--make-bibliography (filename)) + (:copier nil)) + "Cached bibliography file." + (filename + nil + :read-only t + :documentation + "True filename of a bibliography, as returned by `file-truename`.") + (hash + nil + :documentation + "Hash of the file's contents, as returned by `buffer-hash`.") + (buffers + nil + :documentation + "List of buffers that require this bibliography.") + (entries + (make-hash-table :test 'equal) + :documentation + "Hash table mapping citation keys to bibliography entries, + as returned by `parsebib-parse`.") + (preformatted + (make-hash-table :test 'equal) + :documentation + "Pre-formatted strings used to display bibliography entries; + see `citar--preformatter`.") + (format-string + nil + :documentation + "Format string used to generate pre-formatted strings.")) + +(defvar citar--bibliography-cache (make-hash-table :test 'equal) "Cache for parsed bibliography files. This is an association list following the pattern: (FILE-ID . ENTRIES) @@ -423,8 +452,114 @@ the bibliography file, and HASH a hash of its contents. ENTRIES is a hash table with citation references as keys and fields alist as values.") -(defvar citar--completion-cache (make-hash-table :test #'equal) - "Hash with key as completion string, value as citekey.") +(defun citar--get-bibliography (filename &optional buffer) + "Return cached bibliography for FILENAME and associate it with BUFFER. +If FILENAME is not already cached, read and cache it. If BUFFER +is nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires the bibliography FILENAME, or +a symbol like 'global." + (let* ((buffer (cond ((null buffer) (current-buffer)) + ((symbolp buffer) buffer) + (t (get-buffer buffer)))) + (cached (gethash filename citar--bibliography-cache)) + (bib (or cached (citar--make-bibliography filename)))) + (unless cached + (citar--update-bibliography bib) + (puthash filename bib citar--bibliography-cache)) + (cl-pushnew buffer (citar--bibliography-buffers bib)) + (unless (symbolp buffer) + (with-current-buffer buffer + (dolist (hook '(change-major-mode-hook kill-buffer-hook)) + (add-hook hook #'citar--release-bibliographies 0 'local)))) + bib)) + +(defun citar--cache-bibliographies (filenames &optional buffer) + "Return cached bibliographies for FILENAMES and associate them with BUFFER. +FILENAMES is a list of bibliography file names. If BUFFER is +nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires these bibliographies, or a +symbol like 'global. + +Remove any existing associations between BUFFER and cached files +not included in FILENAMES. Release cached files that are no +longer needed by any other buffer. + +Return a list of `citar--bibliography` objects, one for each +element of FILENAMES." + (citar--release-bibliographies filenames buffer) + (mapcar + (lambda (filename) (citar--get-bibliography filename buffer)) + filenames)) + +(defun citar--release-bibliographies (&optional keep-filenames buffer) + "Dissociate BUFFER from cached bibliographies. +If BUFFER is nil, use the current buffer. Otherwise, BUFFER +should be a buffer object, buffer name, or a symbol like 'global. +KEEP-FILENAMES is a list of file names that are not dissociated +from BUFFER. + +Remove any bibliographies from the cache that are no longer +needed by any other buffer." + (let ((buffer (cond ((null buffer) (current-buffer)) + ((symbolp buffer) buffer) + (t (get-buffer buffer))))) + (maphash + (lambda (filename bib) + (unless (member filename keep-filenames) + (setf (citar--bibliography-buffers bib) + (delq buffer (citar--bibliography-buffers bib))) + (unless (citar--bibliography-buffers bib) + (citar--delete-bibliography-from-cache filename)))) + citar--bibliography-cache))) + +(defun citar--bibliographies (&rest buffers) + "Return bibliographies for BUFFERS." + (delete-dups + (mapcan + (lambda (buffer) + (citar--cache-bibliographies (citar--bibliography-files buffer) buffer)) + (or buffers (list (current-buffer) 'global))))) + +(defun citar--delete-bibliography-from-cache (filename) + "Remove bibliography cache entry for FILENAME." + ;; TODO Perform other needed actions, like removing filenotify watches + (remhash filename citar--bibliography-cache)) + +(defun citar--update-bibliography (bib &optional force) + "Update the bibliography BIB from the original file. + +Unless FORCE is non-nil, the file is re-read only if it has been +modified since the last time BIB was updated." + (let* ((filename (citar--bibliography-filename bib)) + (entries (citar--bibliography-entries bib)) + (preformatted (citar--bibliography-preformatted bib)) + (formatstring (citar--bibliography-format-string bib)) + (newformatstring + (concat (propertize (citar--get-template 'main) 'face 'citar-highlight) + (propertize (citar--get-template 'suffix) 'face 'citar))) + (newhash + (with-temp-buffer + (insert-file-contents filename) + (buffer-hash)))) + ;; TODO Also check file size and modification time before hashing? + ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` + (unless (or force + (and (equal newhash (citar--bibliography-hash bib)) + (equal-including-properties formatstring newformatstring))) + ;; Update entries + (clrhash entries) + (parsebib-parse filename + :entries entries + :fields (citar--fields-to-parse)) + (setf (citar--bibliography-hash bib) newhash) + ;; Update preformatted strings + (clrhash preformatted) + (let ((preformatter (citar--preformatter newformatstring))) + (maphash + (lambda (citekey entry) + (puthash citekey (funcall preformatter entry) preformatted)) + entries)) + (setf (citar--bibliography-format-string bib) newformatstring)))) ;;; Completion functions @@ -596,9 +731,22 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) -(defun citar--bibliography-files () - "The list of global and local bibliography files." - (seq-concatenate 'list citar-bibliography (citar--local-files-to-cache))) +(defun citar--bibliography-files (&rest buffers) + "Bibliography file names for BUFFERS. +The elements of BUFFERS are either buffers or the symbol 'global. +Returns the absolute file names of the bibliographies in all +these contexts. + +When BUFFERS is empty, return local bibliographies for the +current buffer and global bibliographies." + (citar-file--normalize-paths + (mapcan (lambda (buffer) + (if (eq buffer 'global) + (if (listp citar-bibliography) citar-bibliography + (list citar-bibliography)) + (with-current-buffer buffer + (citar--major-mode-function 'local-bib-files #'ignore)))) + (or buffers (list (current-buffer) 'global))))) (defun citar--ref-completion-table () "Return completion table for cite keys, as a hash table. @@ -615,10 +763,10 @@ Return nil if there are no bibliography files or no entries." (starwidth (- (frame-width) (+ 2 symbolswidth mainwidth suffixwidth)))) (cond - ((null entries) nil) ; no bibliography files + ((null entries) nil) ; no bibliography files ;; if completion-cache is same as bibliography-cache, use the former ((gethash entries citar--completion-cache) - citar--completion-cache) ; REVIEW ? + citar--completion-cache) ; REVIEW ? (t (clrhash citar--completion-cache) (dolist (key (citar--all-keys)) @@ -649,37 +797,6 @@ Return nil if there are no bibliography files or no entries." (puthash entries t citar--completion-cache) ; REVIEW ? citar--completion-cache))))) -;; adapted from 'org-cite-basic--parse-bibliography' -(defvar citar--file-id-cache nil - "Hash table linking files to their hash.") - -(defun citar--parse-bibliography () - "List all entries available in the buffer. -Each association follows the pattern - (FILE . ENTRIES) -where FILE is the absolute file name of the bibliography file, -and ENTRIES is a hash table where keys are references and values -are association lists between fields, as symbols, and values as -strings or nil." - (unless (hash-table-p citar--file-id-cache) - (setq citar--file-id-cache (make-hash-table :test #'equal))) - (let ((results nil)) - (dolist (file (citar--bibliography-files)) - (when (file-readable-p file) - (with-temp-buffer - (when (or (file-has-changed-p file) - (not (gethash file citar--file-id-cache))) - (insert-file-contents file) - (puthash file (md5 (current-buffer)) citar--file-id-cache)) - (let* ((file-id (cons file (gethash file citar--file-id-cache))) - (entries - (or (cdr (assoc file-id citar--bibliography-cache)) - (let ((table (parsebib-parse file))) - (push (cons file-id table) citar--bibliography-cache) - table)))) - (push (cons file entries) results))))) - results)) - (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. If no function is found matching KEY for the current major mode, @@ -699,14 +816,6 @@ return DEFAULT." If no function is found, the DEFAULT function is called." (apply (citar--get-major-mode-function key default) args)) -(defun citar--local-files-to-cache () - "The local bibliographic files not included in the global bibliography." - ;; We cache these locally to the buffer. - (seq-difference (citar-file--normalize-paths - (citar--major-mode-function 'local-bib-files #'ignore)) - (citar-file--normalize-paths - citar-bibliography))) - ;; Data access functions (cl-defun citar-get-data-entries (&optional &key filter) @@ -722,15 +831,24 @@ If no function is found, the DEFAULT function is called." (cdr bibliography))) results)) -(defun citar--get-entry (key) +(defun citar--get-entry (key &optional bibs) "Return entry for KEY, as an association list." (catch :found ;; Iterate through the cached bibliography hashes and find a key. - (pcase-dolist (`(,_ . ,entries) (citar--parse-bibliography)) - (let ((entry (gethash key entries))) + (dolist (bib (or bibs (citar--bibliographies))) + (let* ((entries (citar--bibliography-entries bib)) + (entry (gethash key entries))) (when entry (throw :found entry)))) nil)) +(defun citar--get-entries (&optional bibs) + (let ((entries (make-hash-table :test 'equal))) + (dolist (bib (reverse (or bibs (citar--bibliographies)))) + (maphash (lambda (citekey entry) + (puthash citekey entry entries)) + (citar--bibliography-entries bib))) + entries)) + (defun citar--get-value (field key-or-entry) "Return FIELD value for KEY-OR-ENTRY." (let ((entry (if (stringp key-or-entry) @@ -909,6 +1027,99 @@ repeatedly." ;;; Formatting functions +(defun citar--format-parse (format-string) + "Parse FORMAT-STRING." + (let ((regex (concat "\\${" ; ${ + "\\(.*?\\)" ; field names + "\\(?::[[:blank:]]*" ; : + space + "\\(.*?\\)" ; format spec + "[[:blank:]]*\\)?}")) ; space + } + (position 0) + (parsed nil)) + (while (string-match regex format-string position) + (let* ((begin (match-beginning 0)) + (end (match-end 0)) + (textprops (text-properties-at begin format-string)) + (fields (match-string-no-properties 1 format-string)) + (spec (match-string-no-properties 2 format-string)) + (width (cond + ((null spec) nil) + ((equal spec "*") '*) + (t (string-to-number spec))))) + (when (< position begin) + (push (substring format-string position begin) parsed)) + (setq position end) + (push (cons (nconc (when width `(:width ,width)) + (when textprops `(:text-properties ,textprops))) + (split-string-and-unquote fields)) + parsed))) + (when (< position (length format-string)) + (push (substring format-string position) parsed)) + (nreverse parsed))) + +(defun citar--preformat-parse (format-string) + "Parse and group FORMAT-STRING." + (let ((parsed (citar--format-parse format-string)) + groups group props) + (dolist (field parsed) + (unless props (setq props (list :width 0))) + (let ((fieldwidth (cond + ((stringp field) (string-width field)) + ((listp field) (plist-get (car field) :width))))) + (cond + ((eq fieldwidth '*) + (push (cons props (nreverse group)) groups) + (setcar field (plist-put (car field) :width nil)) + (push (cons '(:width *) (list field)) groups) + (setq group nil props nil)) + ((numberp fieldwidth) + (push field group) + (if-let* ((oldwidth (plist-get props :width)) + ((numberp oldwidth))) + (setq props (plist-put props :width (+ oldwidth fieldwidth))))) + (t + (push field group) + (setq props (plist-put props :width nil)))))) + (when group + (push (cons props (nreverse group)) groups)) + (nreverse groups))) + +(defun citar--preformatter (format-string) + "Preformat according to FORMAT-STRING." + (let ((groups (citar--preformat-parse format-string))) + (lambda (entry) + (mapcar + (pcase-lambda (`(,props . ,group)) + (citar--format-string-with-props + props + (mapconcat + (lambda (field) + (if (stringp field) + field + (apply #'citar--format-entry-with-props entry field))) + group ""))) + groups)))) + +(defun citar--format-preformatted (format-string) + (let* ((groups (citar--preformat-parse format-string)) + (widths (mapcar (lambda (group) (plist-get (car group) :width)) + groups)) + (nstars (cl-count '* ))) + (lambda (preformatted)))) + +(defun citar--format-string-with-props (props string) + (let ((width (plist-get props :width)) + (textprops (plist-get props :text-properties))) + (when textprops + (setq string (apply #'propertize string textprops))) + (when (numberp width) + (setq string (truncate-string-to-width string width 0 ?\s ))) + string)) + +(defun citar--format-entry-with-props (entry props &rest fields) + "Format FIELDS of ENTRY using PROPS." + (citar--format-string-with-props props (citar--display-value fields entry))) + (defun citar--format-width (format-string) "Calculate minimal width needed by the FORMAT-STRING." (let ((content-width (apply #'+ From 5821b67e73e75a75b36aa8a5e8bc46735a1482de Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 13 Jun 2022 01:23:50 -0600 Subject: [PATCH 06/78] Move cache and formatting functions into new files --- citar-cache.el | 239 +++++++++++++++++++++++++ citar-file.el | 1 - citar-format.el | 225 +++++++++++++++++++++++ citar-org.el | 7 +- citar.el | 461 ++++++++---------------------------------------- 5 files changed, 538 insertions(+), 395 deletions(-) create mode 100644 citar-cache.el create mode 100644 citar-format.el diff --git a/citar-cache.el b/citar-cache.el new file mode 100644 index 00000000..784ba4b0 --- /dev/null +++ b/citar-cache.el @@ -0,0 +1,239 @@ +;;; citar-cache.el --- Cache functions for citar -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Bruce D'Arcus, Roshan Shariff +;; +;; This file is not part of GNU Emacs. +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . +;; +;;; Commentary: +;; +;; Functions for caching bibliography files. +;; +;;; Code: + +(eval-when-compile + (require 'cl-lib)) +(require 'parsebib) +(require 'citar-format) + +(declare-function citar--get-template "citar") +(declare-function citar--fields-to-parse "citar") + + +;;; Variables: + + +(defvar citar-cache--bibliographies (make-hash-table :test 'equal) + "Cache for parsed bibliography files. +This is an association list following the pattern: + (FILE-ID . ENTRIES) +FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of +the bibliography file, and HASH a hash of its contents. +ENTRIES is a hash table with citation references as keys and fields alist as +values.") + + +;;; Bibliography objects + + +(cl-defstruct (citar-cache--bibliography + (:constructor citar-cache--make-bibliography (filename)) + (:copier nil)) + "Cached bibliography file." + (filename + nil + :read-only t + :documentation + "True filename of a bibliography, as returned by `file-truename`.") + (hash + nil + :documentation + "Hash of the file's contents, as returned by `buffer-hash`.") + (buffers + nil + :documentation + "List of buffers that require this bibliography.") + (entries + (make-hash-table :test 'equal) + :documentation + "Hash table mapping citation keys to bibliography entries, + as returned by `parsebib-parse`.") + (preformatted + (make-hash-table :test 'equal) + :documentation + "Pre-formatted strings used to display bibliography entries; + see `citar--preformatter`.") + (format-string + nil + :documentation + "Format string used to generate pre-formatted strings.")) + + +(defun citar-cache--get-bibliographies (filenames &optional buffer) + "Return cached bibliographies for FILENAMES and associate them with BUFFER. +FILENAMES is a list of bibliography file names. If BUFFER is +nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires these bibliographies, or a +symbol like 'global. + +Remove any existing associations between BUFFER and cached files +not included in FILENAMES. Release cached files that are no +longer needed by any other buffer. + +Return a list of `citar--bibliography` objects, one for each +element of FILENAMES." + (citar-cache--release-bibliographies filenames buffer) + (mapcar + (lambda (filename) (citar-cache--get-bibliography filename buffer)) + filenames)) + +(defun citar-cache--entry (key bibs) + (catch :found + ;; Iterate through the cached bibliography hashes and find a key. + (dolist (bib bibs) + (let* ((entries (citar-cache--bibliography-entries bib)) + (entry (gethash key entries))) + (when entry (throw :found entry)))) + nil)) + +(defun citar-cache--entries (bibs) + (citar-cache--merge-hash-tables + (mapcar #'citar-cache--bibliography-entries bibs))) + +(defun citar-cache--preformatted (bibs) + (citar-cache--merge-hash-tables + (mapcar #'citar-cache--bibliography-preformatted bibs))) + + +;;; Creating and deleting bibliography caches + + +(defun citar-cache--get-bibliography (filename &optional buffer) + "Return cached bibliography for FILENAME and associate it with BUFFER. +If FILENAME is not already cached, read and cache it. If BUFFER +is nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires the bibliography FILENAME, or +a symbol like 'global." + (let* ((cached (gethash filename citar-cache--bibliographies)) + (bib (or cached (citar-cache--make-bibliography filename))) + (buffer (citar-cache--canonicalize-buffer buffer)) + (fmtstr (citar--get-template 'completion))) + (unless cached + (setf (citar-cache--bibliography-format-string bib) fmtstr) + (citar-cache--update-bibliography bib) + (puthash filename bib citar-cache--bibliographies)) + ;; Preformat strings if format has changed + (unless (equal-including-properties + fmtstr (citar-cache--bibliography-format-string bib)) + (setf (citar-cache--bibliography-format-string bib) fmtstr) + (citar-cache--preformat-bibliography bib)) + ;; Associate buffer with this bibliography: + (cl-pushnew buffer (citar-cache--bibliography-buffers bib)) + ;; Release bibliography when buffer is killed or changes major mode: + (unless (symbolp buffer) + (with-current-buffer buffer + (dolist (hook '(change-major-mode-hook kill-buffer-hook)) + (add-hook hook #'citar-cache--release-bibliographies 0 'local)))) + bib)) + + +(defun citar-cache--release-bibliographies (&optional keep-filenames buffer) + "Dissociate BUFFER from cached bibliographies. +If BUFFER is nil, use the current buffer. Otherwise, BUFFER +should be a buffer object, buffer name, or a symbol like 'global. +KEEP-FILENAMES is a list of file names that are not dissociated +from BUFFER. + +Remove any bibliographies from the cache that are no longer +needed by any other buffer." + (let ((buffer (citar-cache--canonicalize-buffer buffer))) + (maphash + (lambda (filename bib) + (unless (member filename keep-filenames) + (cl-callf2 delq buffer (citar-cache--bibliography-buffers bib)) + (unless (citar-cache--bibliography-buffers bib) + (citar-cache--remove-bibliography filename)))) + citar-cache--bibliographies))) + + +(defun citar-cache--remove-bibliography (filename) + "Remove bibliography cache entry for FILENAME." + ;; TODO Perform other needed actions, like removing filenotify watches + (remhash filename citar-cache--bibliographies)) + + +;;; Updating bibliographies + + +(defun citar-cache--update-bibliography (bib &optional force) + "Update the bibliography BIB from the original file. + +Unless FORCE is non-nil, the file is re-read only if it has been +modified since the last time BIB was updated." + (let* ((filename (citar-cache--bibliography-filename bib)) + (entries (citar-cache--bibliography-entries bib)) + (newhash (with-temp-buffer + (insert-file-contents filename) + (buffer-hash)))) + ;; TODO Also check file size and modification time before hashing? + ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` + (unless (or force (equal newhash (citar-cache--bibliography-hash bib))) + ;; Update entries + (clrhash entries) + (parsebib-parse filename :entries entries :fields (citar--fields-to-parse)) + (setf (citar-cache--bibliography-hash bib) newhash) + ;; Update preformatted strings + (citar-cache--preformat-bibliography bib)))) + + +(defun citar-cache--preformat-bibliography (bib) + "Updated pre-formatted strings in BIB." + (let* ((entries (citar-cache--bibliography-entries bib)) + (fmtstr (citar-cache--bibliography-format-string bib)) + (preformat (citar-format--preformat fmtstr :hide-elided t)) + (preformatted (citar-cache--bibliography-preformatted bib))) + (clrhash preformatted) + (maphash (lambda (citekey entry) + (puthash citekey (funcall preformat entry) preformatted)) + entries))) + + +;;; Utility functions: + + +(defun citar-cache--canonicalize-buffer (buffer) + "Return buffer object or symbol denoted by BUFFER. +If BUFFER is nil, return the current buffer. Otherwise, BUFFER +should be a buffer object or name, or a symbol like 'global. If +it is a buffer object or symbol, it is returned as-is. +Otherwise, return the buffer object whose name is BUFFER." + (cond ((null buffer) (current-buffer)) + ((symbolp buffer) buffer) + (t (get-buffer buffer)))) + + +(defun citar-cache--merge-hash-tables (hash-tables) + "Merge hash tables in HASH-TABLES." + (when-let ((hash-tables (reverse hash-tables)) + (first (pop hash-tables))) + (if (null hash-tables) + first + (let ((result (copy-hash-table first))) + (dolist (table hash-tables result) + (maphash (lambda (key entry) (puthash key entry result)) table)))))) + + +(provide 'citar-cache) +;;; citar-cache.el ends here diff --git a/citar-file.el b/citar-file.el index 75e0dc3f..30d2d717 100644 --- a/citar-file.el +++ b/citar-file.el @@ -41,7 +41,6 @@ (declare-function citar--get-entry "citar") (declare-function citar--get-value "citar") (declare-function citar--get-template "citar") -(declare-function citar--format-entry-no-widths "citar") ;;;; File related variables diff --git a/citar-format.el b/citar-format.el new file mode 100644 index 00000000..73f8eabe --- /dev/null +++ b/citar-format.el @@ -0,0 +1,225 @@ +;;; citar-format.el --- Formatting functions for citar -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Bruce D'Arcus, Roshan Shariff +;; +;; This file is not part of GNU Emacs. +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . +;; +;;; Commentary: +;; +;; Functions for formatting bibliography entries. +;; +;;; Code: + +(eval-when-compile + (require 'cl-lib)) + +(declare-function citar--display-value "citar") +(defvar citar-ellipsis) + + +;;; Formatting bibliography entries + + +(cl-defun citar-format--entry (format-string entry &key width hide-elided + (ellipsis citar-ellipsis)) + "Format ENTRY according to FORMAT-STRING." + (cl-flet ((getwidth (fieldspec) + (unless (stringp fieldspec) + (plist-get (car fieldspec) :width))) + (fmtfield (fieldspec) + (citar-format--fieldspec fieldspec entry + :hide-elided hide-elided + :ellipsis ellipsis))) + (let* ((fieldspecs (citar-format--parse format-string)) + (widths (mapcar #'getwidth fieldspecs)) + (strings (mapcar #'fmtfield fieldspecs))) + (citar-format--star-widths widths strings :width width + :hide-elided hide-elided :ellipsis ellipsis)))) + + +;;; Pre-formatting bibliography entries + + +(cl-defun citar-format--preformat (format-string &key hide-elided + (ellipsis citar-ellipsis)) + "Preformat according to FORMAT-STRING. +See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." + (let ((fieldgroups (citar-format--preformat-parse format-string))) + (lambda (entry) + (cl-flet ((fmtfield (fieldspec) + (citar-format--fieldspec fieldspec entry + :hide-elided hide-elided + :ellipsis ellipsis))) + (mapcar (lambda (groupspec) + (mapconcat #'fmtfield (cdr groupspec) "")) + fieldgroups))))) + + +(cl-defun citar-format--preformatted (format-string &key width hide-elided + (ellipsis citar-ellipsis)) + "Fit pre-formatted strings to WIDTH according to FORMAT-STRING. +See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." + (let* ((fieldgroups (citar-format--preformat-parse format-string)) + (widths (mapcar (lambda (groupspec) + (plist-get (car groupspec) :width)) + fieldgroups))) + (lambda (preformatted) + (citar-format--star-widths widths preformatted :width width + :hide-elided hide-elided :ellipsis ellipsis)))) + + +;;; Internal implementation functions + + +(cl-defun citar-format--fieldspec (fieldspec entry &key hide-elided ellipsis) + "Format FIELDSPEC using information from ENTRY. +See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." + (if (stringp fieldspec) + fieldspec + (let* ((fmtprops (car fieldspec)) + (fieldnames (cdr fieldspec)) + (displaystr (citar--display-value fieldnames entry))) + (apply #'citar-format--string displaystr + :hide-elided hide-elided :ellipsis ellipsis + fmtprops)))) + + +(cl-defun citar-format--string (string + &key width text-properties hide-elided ellipsis) + "Truncate STRING to WIDTH and apply TEXT-PROPERTIES. +If HIDE-ELIDED is non-nil, the truncated part of STRING is +covered by a display property that makes it invisible, instead of +being deleted. ELLIPSIS, when non-nil, specifies a string to +display instead of the truncated part of the text." + (when text-properties + (setq string (apply #'propertize string text-properties))) + (when (numberp width) + (setq string (truncate-string-to-width string width 0 ?\s ellipsis hide-elided))) + string) + + +(cl-defun citar-format--star-widths (widths strings &key width + hide-elided ellipsis) + "Format STRINGS according to WIDTHS to fit WIDTH." + (if (not (and (numberp width) (cl-find '* widths))) + ;; If width is unlimited or there are no *-width fields, just join strings. + ;; We only support truncating *-width fields. + (string-join strings) + ;; Otherwise, calculate extra space available for *-width fields + (let ((usedwidth 0) (nstars 0)) + ;; For fields without width spec, add their actual width to usedwidth + (cl-mapc + (lambda (width string) + (cond ((eq '* width) (cl-incf nstars)) + ((numberp width) (cl-incf usedwidth width)) + ((null width) (cl-incf usedwidth (string-width string))))) + widths strings) + (let* ((extrawidth (max 0 (- width usedwidth))) + (starwidth (/ extrawidth nstars)) + (remainder (% extrawidth nstars)) + (starindex 0)) + (string-join + (cl-mapcar + (lambda (width string) + (if (not (eq width '*)) + string + (cl-incf starindex) + (citar-format--string + string + :width (+ starwidth (if (<= starindex remainder) 1 0)) + :hide-elided hide-elided :ellipsis ellipsis))) + widths strings)))))) + + +;;; Parsing format strings + + +(defun citar-format--parse (format-string) + "Parse FORMAT-STRING." + (let ((regex (concat "\\${" ; ${ + "\\(.*?\\)" ; field names + "\\(?::[[:blank:]]*" ; : + space + "\\(.*?\\)" ; format spec + "[[:blank:]]*\\)?}")) ; space + } + (position 0) + (fieldspecs nil)) + (while (string-match regex format-string position) + (let* ((begin (match-beginning 0)) + (end (match-end 0)) + (textprops (text-properties-at begin format-string)) + (fieldnames (match-string-no-properties 1 format-string)) + (spec (match-string-no-properties 2 format-string)) + (width (cond + ((or (null spec) (string-empty-p spec)) nil) + ((string-equal spec "*") '*) + (t (string-to-number spec))))) + (when (< position begin) + (push (substring format-string position begin) fieldspecs)) + (push (cons (nconc (when width `(:width ,width)) + (when textprops `(:text-properties ,textprops))) + (split-string-and-unquote fieldnames)) + fieldspecs) + (setq position end))) + (when (< position (length format-string)) + (push (substring format-string position) fieldspecs)) + (nreverse fieldspecs))) + + +(defun citar-format--preformat-parse (format-string) + "Parse and group FORMAT-STRING." + (let (fieldgroups group (groupwidth 0)) + (cl-flet ((newgroup () + (when group + (push (cons (when groupwidth `(:width ,groupwidth)) + (nreverse group)) + fieldgroups) + (setq group nil)) + (setq groupwidth 0))) + (dolist (fieldspec (citar-format--parse format-string)) + (let ((fieldwidth (cond + ((stringp fieldspec) (string-width fieldspec)) + ((listp fieldspec) (plist-get (car fieldspec) :width))))) + (cond + ((eq fieldwidth '*) + ;; *-width field; start a new group + (newgroup) + ;; Pre-format the field at unlimited width by setting :width to nil + (cl-callf plist-put (car fieldspec) :width nil) + ;; Add the field in its own pre-format group with :width * + (push fieldspec group) + (setq groupwidth '*) + (newgroup)) + ((numberp fieldwidth) + ;; Fixed-length field; start new group if needed + (unless (numberp groupwidth) + (newgroup)) + ;; Add field to group and increment group width + (push fieldspec group) + (cl-incf groupwidth fieldwidth)) + (t + ;; Unknown-length field; start new group if needed + (unless (null groupwidth) + (newgroup)) + ;; Add field to group; group width is now unknown + (push fieldspec group) + (setq groupwidth nil))))) + ;; Add any remaining fields to group + (newgroup)) + (nreverse fieldgroups))) + + +(provide 'citar-format) +;;; citar-format.el ends here diff --git a/citar-org.el b/citar-org.el index b04327d5..e76979ff 100644 --- a/citar-org.el +++ b/citar-org.el @@ -292,11 +292,8 @@ With optional argument FORCE, force the creation of a new ID." (defun citar-org-format-note-default (key entry filepath) "Format a note FILEPATH from KEY and ENTRY." (let* ((template (citar--get-template 'note)) - (note-meta - (when template - (citar--format-entry-no-widths - entry - template))) + (note-meta (when template + (citar-format--entry template entry))) (buffer (find-file filepath))) (with-current-buffer buffer ;; This just overrides other template insertion. diff --git a/citar.el b/citar.el index e5f8542a..b01263c4 100644 --- a/citar.el +++ b/citar.el @@ -41,8 +41,9 @@ (require 'subr-x)) (require 'seq) (require 'browse-url) +(require 'citar-cache) +(require 'citar-format) (require 'citar-file) -(require 'parsebib) (require 'crm) ;;; pre-1.0 API cleanup @@ -152,6 +153,19 @@ for the title field for new notes." :value-type string :options (main suffix preview note))) +(defcustom citar-ellipsis nil + "Ellipsis string to mark ending of truncated display fields. + +If t, use the value of `truncate-string-ellipsis`. If nil, no +ellipsis will be used. Otherwise, this should be a non-empty +string specifying the ellipsis." + :group 'citar + :type '(choice (const :tag "Use 'truncate-string-ellipsis'" t) + (const :tag "Disable ellipsis" nil) + (const "...") + (const "…") + (string :tag "Ellipsis string"))) + (defcustom citar-format-reference-function #'citar-format-reference "Function used to render formatted references. @@ -260,8 +274,7 @@ If nil, single resources will open without prompting." :group 'citar :type '(function)) -(defcustom citar-has-note-functions - '(citar--has-file-notes) +(defcustom citar-has-note-functions '(citar-file-has-notes) "Functions used for displaying note indicators. Such functions must take KEY and return non-nil when the @@ -411,156 +424,31 @@ When nil, all citar commands will use `completing-read`." ;;; Bibliography cache -(cl-defstruct (citar--bibliography - (:constructor citar--make-bibliography (filename)) - (:copier nil)) - "Cached bibliography file." - (filename - nil - :read-only t - :documentation - "True filename of a bibliography, as returned by `file-truename`.") - (hash - nil - :documentation - "Hash of the file's contents, as returned by `buffer-hash`.") - (buffers - nil - :documentation - "List of buffers that require this bibliography.") - (entries - (make-hash-table :test 'equal) - :documentation - "Hash table mapping citation keys to bibliography entries, - as returned by `parsebib-parse`.") - (preformatted - (make-hash-table :test 'equal) - :documentation - "Pre-formatted strings used to display bibliography entries; - see `citar--preformatter`.") - (format-string - nil - :documentation - "Format string used to generate pre-formatted strings.")) - -(defvar citar--bibliography-cache (make-hash-table :test 'equal) - "Cache for parsed bibliography files. -This is an association list following the pattern: - (FILE-ID . ENTRIES) -FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of -the bibliography file, and HASH a hash of its contents. -ENTRIES is a hash table with citation references as keys and fields alist as -values.") - -(defun citar--get-bibliography (filename &optional buffer) - "Return cached bibliography for FILENAME and associate it with BUFFER. -If FILENAME is not already cached, read and cache it. If BUFFER -is nil, use the current buffer. Otherwise, BUFFER should be a -buffer object or name that requires the bibliography FILENAME, or -a symbol like 'global." - (let* ((buffer (cond ((null buffer) (current-buffer)) - ((symbolp buffer) buffer) - (t (get-buffer buffer)))) - (cached (gethash filename citar--bibliography-cache)) - (bib (or cached (citar--make-bibliography filename)))) - (unless cached - (citar--update-bibliography bib) - (puthash filename bib citar--bibliography-cache)) - (cl-pushnew buffer (citar--bibliography-buffers bib)) - (unless (symbolp buffer) - (with-current-buffer buffer - (dolist (hook '(change-major-mode-hook kill-buffer-hook)) - (add-hook hook #'citar--release-bibliographies 0 'local)))) - bib)) - -(defun citar--cache-bibliographies (filenames &optional buffer) - "Return cached bibliographies for FILENAMES and associate them with BUFFER. -FILENAMES is a list of bibliography file names. If BUFFER is -nil, use the current buffer. Otherwise, BUFFER should be a -buffer object or name that requires these bibliographies, or a -symbol like 'global. - -Remove any existing associations between BUFFER and cached files -not included in FILENAMES. Release cached files that are no -longer needed by any other buffer. - -Return a list of `citar--bibliography` objects, one for each -element of FILENAMES." - (citar--release-bibliographies filenames buffer) - (mapcar - (lambda (filename) (citar--get-bibliography filename buffer)) - filenames)) - -(defun citar--release-bibliographies (&optional keep-filenames buffer) - "Dissociate BUFFER from cached bibliographies. -If BUFFER is nil, use the current buffer. Otherwise, BUFFER -should be a buffer object, buffer name, or a symbol like 'global. -KEEP-FILENAMES is a list of file names that are not dissociated -from BUFFER. - -Remove any bibliographies from the cache that are no longer -needed by any other buffer." - (let ((buffer (cond ((null buffer) (current-buffer)) - ((symbolp buffer) buffer) - (t (get-buffer buffer))))) - (maphash - (lambda (filename bib) - (unless (member filename keep-filenames) - (setf (citar--bibliography-buffers bib) - (delq buffer (citar--bibliography-buffers bib))) - (unless (citar--bibliography-buffers bib) - (citar--delete-bibliography-from-cache filename)))) - citar--bibliography-cache))) +(defun citar--bibliography-files (&rest buffers) + "Bibliography file names for BUFFERS. +The elements of BUFFERS are either buffers or the symbol 'global. +Returns the absolute file names of the bibliographies in all +these contexts. + +When BUFFERS is empty, return local bibliographies for the +current buffer and global bibliographies." + (citar-file--normalize-paths + (mapcan (lambda (buffer) + (if (eq buffer 'global) + (if (listp citar-bibliography) citar-bibliography + (list citar-bibliography)) + (with-current-buffer buffer + (citar--major-mode-function 'local-bib-files #'ignore)))) + (or buffers (list (current-buffer) 'global))))) (defun citar--bibliographies (&rest buffers) "Return bibliographies for BUFFERS." (delete-dups (mapcan (lambda (buffer) - (citar--cache-bibliographies (citar--bibliography-files buffer) buffer)) + (citar-cache--get-bibliographies (citar--bibliography-files buffer) buffer)) (or buffers (list (current-buffer) 'global))))) -(defun citar--delete-bibliography-from-cache (filename) - "Remove bibliography cache entry for FILENAME." - ;; TODO Perform other needed actions, like removing filenotify watches - (remhash filename citar--bibliography-cache)) - -(defun citar--update-bibliography (bib &optional force) - "Update the bibliography BIB from the original file. - -Unless FORCE is non-nil, the file is re-read only if it has been -modified since the last time BIB was updated." - (let* ((filename (citar--bibliography-filename bib)) - (entries (citar--bibliography-entries bib)) - (preformatted (citar--bibliography-preformatted bib)) - (formatstring (citar--bibliography-format-string bib)) - (newformatstring - (concat (propertize (citar--get-template 'main) 'face 'citar-highlight) - (propertize (citar--get-template 'suffix) 'face 'citar))) - (newhash - (with-temp-buffer - (insert-file-contents filename) - (buffer-hash)))) - ;; TODO Also check file size and modification time before hashing? - ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` - (unless (or force - (and (equal newhash (citar--bibliography-hash bib)) - (equal-including-properties formatstring newformatstring))) - ;; Update entries - (clrhash entries) - (parsebib-parse filename - :entries entries - :fields (citar--fields-to-parse)) - (setf (citar--bibliography-hash bib) newhash) - ;; Update preformatted strings - (clrhash preformatted) - (let ((preformatter (citar--preformatter newformatstring))) - (maphash - (lambda (citekey entry) - (puthash citekey (funcall preformatter entry) preformatted)) - entries)) - (setf (citar--bibliography-format-string bib) newformatstring)))) - ;;; Completion functions (defun citar--completion-table (candidates &optional filter &rest metadata) @@ -731,71 +619,34 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) -(defun citar--bibliography-files (&rest buffers) - "Bibliography file names for BUFFERS. -The elements of BUFFERS are either buffers or the symbol 'global. -Returns the absolute file names of the bibliographies in all -these contexts. - -When BUFFERS is empty, return local bibliographies for the -current buffer and global bibliographies." - (citar-file--normalize-paths - (mapcan (lambda (buffer) - (if (eq buffer 'global) - (if (listp citar-bibliography) citar-bibliography - (list citar-bibliography)) - (with-current-buffer buffer - (citar--major-mode-function 'local-bib-files #'ignore)))) - (or buffers (list (current-buffer) 'global))))) - (defun citar--ref-completion-table () "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and title of the reference. Values are the cite keys. Return nil if there are no bibliography files or no entries." ;; Populate bibliography cache. - (let* ((entries (citar--parse-bibliography)) - (hasnotep (citar-has-note)) - (hasfilep (citar-has-file)) - (mainwidth (citar--format-width (citar--get-template 'main))) - (suffixwidth (citar--format-width (citar--get-template 'suffix))) - (symbolswidth (string-width (citar--symbols-string t t t))) - (starwidth - (- (frame-width) (+ 2 symbolswidth mainwidth suffixwidth)))) - (cond - ((null entries) nil) ; no bibliography files - ;; if completion-cache is same as bibliography-cache, use the former - ((gethash entries citar--completion-cache) - citar--completion-cache) ; REVIEW ? - (t - (clrhash citar--completion-cache) - (dolist (key (citar--all-keys)) - (let* ((entry (citar--get-entry key)) - (hasfile - (when (funcall hasfilep key entry) "has:file")) - (hasnote - (when (funcall hasnotep key entry) "has:note")) - (candidatemain - (citar--format-entry - entry - starwidth - (citar--get-template 'main))) - (candidatesuffix - (citar--format-entry - entry - starwidth - (citar--get-template 'suffix))) - (invisible (concat hasfile " " hasnote)) - (completion - (string-trim-right - (concat - (propertize candidatemain 'face 'citar-highlight) " " - (propertize candidatesuffix 'face 'citar) - (propertize invisible 'invisible t))))) - (puthash completion key citar--completion-cache))) - (unless (map-empty-p citar--completion-cache) ; no key - (puthash entries t citar--completion-cache) ; REVIEW ? - citar--completion-cache))))) + (when-let ((bibs (citar--bibliographies))) + (let* ((entries (citar-cache--entries bibs)) + (preformatted (citar-cache--preformatted bibs)) + (fmtstr (citar--get-template 'completion)) + (hasnotep (citar-has-note)) + (hasfilep (citar-has-file)) + (hasnotetag (propertize " has:notes" 'invisible t)) + (hasfiletag (propertize " has:files" 'invisible t)) + (symbolswidth (string-width (citar--symbols-string t t t))) + (width (- (frame-width) symbolswidth 2)) + (format (citar-format--preformatted + fmtstr :width width :hide-elided t :ellipsis citar-ellipsis)) + (completions (make-hash-table :test 'equal))) + (maphash + (lambda (citekey preform) + (let* ((entry (gethash citekey entries)) + (cand (concat (string-trim-right (funcall format preform)) + (when (funcall hasnotep citekey entry) hasnotetag) + (when (funcall hasfilep citekey entry) hasfiletag)))) + (puthash cand citekey completions))) + preformatted) + completions))) (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. @@ -831,23 +682,12 @@ If no function is found, the DEFAULT function is called." (cdr bibliography))) results)) -(defun citar--get-entry (key &optional bibs) +(defun citar--get-entry (key) "Return entry for KEY, as an association list." - (catch :found - ;; Iterate through the cached bibliography hashes and find a key. - (dolist (bib (or bibs (citar--bibliographies))) - (let* ((entries (citar--bibliography-entries bib)) - (entry (gethash key entries))) - (when entry (throw :found entry)))) - nil)) - -(defun citar--get-entries (&optional bibs) - (let ((entries (make-hash-table :test 'equal))) - (dolist (bib (reverse (or bibs (citar--bibliographies)))) - (maphash (lambda (citekey entry) - (puthash citekey entry entries)) - (citar--bibliography-entries bib))) - entries)) + (citar-cache--entry key (citar--bibliographies))) + +(defun citar--get-entries () + (citar-cache--entries (citar--bibliographies))) (defun citar--get-value (field key-or-entry) "Return FIELD value for KEY-OR-ENTRY." @@ -991,11 +831,12 @@ repeatedly." (defun citar--get-template (template-name) "Return template string for TEMPLATE-NAME." - (let ((template - (cdr (assoc template-name citar-templates)))) - (unless template - (error "No template for \"%s\" - check variable 'citar-templates'" template-name)) - template)) + (or + (cdr (assq template-name citar-templates)) + (when (eq template-name 'completion) + (concat (propertize (citar--get-template 'main) 'face 'citar-highlight) + (propertize (citar--get-template 'suffix) 'face 'citar))) + (error "No template for \"%s\" - check variable 'citar-templates'" template-name))) (defun citar--all-keys () "List all keys available in current bibliography." @@ -1025,162 +866,6 @@ repeatedly." (search (completing-read "Preset: " citar-presets))) (insert search))) -;;; Formatting functions - -(defun citar--format-parse (format-string) - "Parse FORMAT-STRING." - (let ((regex (concat "\\${" ; ${ - "\\(.*?\\)" ; field names - "\\(?::[[:blank:]]*" ; : + space - "\\(.*?\\)" ; format spec - "[[:blank:]]*\\)?}")) ; space + } - (position 0) - (parsed nil)) - (while (string-match regex format-string position) - (let* ((begin (match-beginning 0)) - (end (match-end 0)) - (textprops (text-properties-at begin format-string)) - (fields (match-string-no-properties 1 format-string)) - (spec (match-string-no-properties 2 format-string)) - (width (cond - ((null spec) nil) - ((equal spec "*") '*) - (t (string-to-number spec))))) - (when (< position begin) - (push (substring format-string position begin) parsed)) - (setq position end) - (push (cons (nconc (when width `(:width ,width)) - (when textprops `(:text-properties ,textprops))) - (split-string-and-unquote fields)) - parsed))) - (when (< position (length format-string)) - (push (substring format-string position) parsed)) - (nreverse parsed))) - -(defun citar--preformat-parse (format-string) - "Parse and group FORMAT-STRING." - (let ((parsed (citar--format-parse format-string)) - groups group props) - (dolist (field parsed) - (unless props (setq props (list :width 0))) - (let ((fieldwidth (cond - ((stringp field) (string-width field)) - ((listp field) (plist-get (car field) :width))))) - (cond - ((eq fieldwidth '*) - (push (cons props (nreverse group)) groups) - (setcar field (plist-put (car field) :width nil)) - (push (cons '(:width *) (list field)) groups) - (setq group nil props nil)) - ((numberp fieldwidth) - (push field group) - (if-let* ((oldwidth (plist-get props :width)) - ((numberp oldwidth))) - (setq props (plist-put props :width (+ oldwidth fieldwidth))))) - (t - (push field group) - (setq props (plist-put props :width nil)))))) - (when group - (push (cons props (nreverse group)) groups)) - (nreverse groups))) - -(defun citar--preformatter (format-string) - "Preformat according to FORMAT-STRING." - (let ((groups (citar--preformat-parse format-string))) - (lambda (entry) - (mapcar - (pcase-lambda (`(,props . ,group)) - (citar--format-string-with-props - props - (mapconcat - (lambda (field) - (if (stringp field) - field - (apply #'citar--format-entry-with-props entry field))) - group ""))) - groups)))) - -(defun citar--format-preformatted (format-string) - (let* ((groups (citar--preformat-parse format-string)) - (widths (mapcar (lambda (group) (plist-get (car group) :width)) - groups)) - (nstars (cl-count '* ))) - (lambda (preformatted)))) - -(defun citar--format-string-with-props (props string) - (let ((width (plist-get props :width)) - (textprops (plist-get props :text-properties))) - (when textprops - (setq string (apply #'propertize string textprops))) - (when (numberp width) - (setq string (truncate-string-to-width string width 0 ?\s ))) - string)) - -(defun citar--format-entry-with-props (entry props &rest fields) - "Format FIELDS of ENTRY using PROPS." - (citar--format-string-with-props props (citar--display-value fields entry))) - -(defun citar--format-width (format-string) - "Calculate minimal width needed by the FORMAT-STRING." - (let ((content-width (apply #'+ - (seq-map #'string-to-number - (split-string format-string ":")))) - (whitespace-width (string-width (citar--format format-string - (lambda (_) ""))))) - (+ content-width whitespace-width))) - -(defun citar--fit-to-width (value width) - "Propertize the string VALUE so that only the WIDTH columns are visible." - (let* ((truncated-value (truncate-string-to-width value width)) - (display-value (truncate-string-to-width truncated-value width 0 ?\s))) - (if (> (string-width value) width) - (concat display-value (propertize (substring value (length truncated-value)) - 'invisible t)) - display-value))) - -(defun citar--format (template replacer) - "Format TEMPLATE with the function REPLACER. -The templates are of form ${foo} for variable foo. -REPLACER takes an argument of the format variable. -Adapted from `org-roam-format-template'." - (replace-regexp-in-string - "\\${\\([^}]+\\)}" - (lambda (md) - (save-match-data - (if-let ((text (funcall replacer (match-string 1 md)))) - text - (signal 'citar-format-resolve md)))) - template - ;; Need literal to make sure it works - t t)) - -(defun citar--format-entry (entry width format-string) - "Formats a BibTeX ENTRY for display in results list. -WIDTH is the width for the * field, and the display format is governed by -FORMAT-STRING." - (citar--format - format-string - (lambda (raw-field) - (let* ((field (split-string raw-field ":")) - (field-names (split-string (car field) "[ ]+")) - (field-width (string-to-number (cadr field))) - (display-width (if (> field-width 0) - ;; If user specifies field width of "*", use - ;; WIDTH; else use the explicit 'field-width'. - field-width - width)) - ;; Make sure we always return a string, even if empty. - (display-value (citar--display-value field-names entry))) - (citar--fit-to-width display-value display-width))))) - -(defun citar--format-entry-no-widths (entry format-string) - "Format ENTRY for display per FORMAT-STRING." - (citar--format - format-string - (lambda (raw-field) - (let ((field-names (split-string raw-field "[ ]+"))) - (citar--display-value field-names entry))))) - ;;; At-point functions for Embark ;;;###autoload @@ -1457,14 +1142,12 @@ ARG is forwarded to the mode-specific insertion function given in (defun citar-format-reference (keys) "Return formatted reference(s) for the elements of KEYS." - (let* ((template (citar--get-template 'preview)) - (references - (with-temp-buffer - (dolist (key keys) - (when template - (insert (citar--format-entry-no-widths key template)))) - (buffer-string)))) - references)) + (let* ((entries (mapcar #'citar--get-entry keys)) + (template (citar--get-template 'preview))) + (with-temp-buffer + (dolist (entry entries) + (insert (citar-format--entry template entry))) + (buffer-string)))) ;;;###autoload (defun citar-insert-keys (keys) From 39ea73a49296894aa5e12099b57c9681358d6e11 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 13 Jun 2022 16:13:58 -0600 Subject: [PATCH 07/78] Remove citar-get-data-entries and citar-all-keys. Do we really need these functions? There are probably better ways of implementing any functionality that would use these. --- citar.el | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/citar.el b/citar.el index b01263c4..393cd283 100644 --- a/citar.el +++ b/citar.el @@ -669,19 +669,6 @@ If no function is found, the DEFAULT function is called." ;; Data access functions -(cl-defun citar-get-data-entries (&optional &key filter) - "Return a subset of entries in bibliography by FILTER. - - (citar-get-data-entries :filter (citar-has-note))" - (let ((results (make-hash-table :test #'equal))) - (dolist (bibliography citar--bibliography-cache) - (maphash - (lambda (citekey entry) - (when (funcall filter citekey) - (puthash citekey entry results))) - (cdr bibliography))) - results)) - (defun citar--get-entry (key) "Return entry for KEY, as an association list." (citar-cache--entry key (citar--bibliographies))) @@ -838,12 +825,6 @@ repeatedly." (propertize (citar--get-template 'suffix) 'face 'citar))) (error "No template for \"%s\" - check variable 'citar-templates'" template-name))) -(defun citar--all-keys () - "List all keys available in current bibliography." - (seq-mapcat (pcase-lambda (`(,_ . ,entries)) - (map-keys entries)) - (citar--parse-bibliography))) - (defun citar--get-link (entry) "Return a link for an ENTRY." (let* ((field (citar--field-with-value '(doi pmid pmcid url) entry)) From 7b0c8254b5f18a1e21499698be3ccde1cb788ba5 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 14 Jun 2022 07:00:20 -0400 Subject: [PATCH 08/78] Update CONTRIBUTING.org --- CONTRIBUTING.org | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org index df68cb37..14bfb9d4 100644 --- a/CONTRIBUTING.org +++ b/CONTRIBUTING.org @@ -11,10 +11,10 @@ If you would like to contribute, details: ** Basic Architecture -Citar has two primary caches, each of which store the data in hash tables: +Citar uses a cache, which stores two hash tables for each bibliography file: -- bibliographic :: keys are citekeys, values are alists of entry fields -- completion :: keys are completion strings, values are citekeys +- entries :: as returned by =parsebib-parse=, keys are citekeys, values are alists of entry fields +- pre-formatted :: values are partially-formatted completion strings The =citar--ref-completion-table= function returns a hash table from the bibliographic cache, and ~citar--get-entry~ and ~-citar--get-value~ provide access to those data. Most user-accessible citar functions take an argument ~key~ or ~keys~. From 40fae077807424fcc919827416306863e82b73c9 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 09:39:23 -0600 Subject: [PATCH 09/78] Update citar-capf.el for new API; still doesn't work for some reason. --- citar-capf.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/citar-capf.el b/citar-capf.el index c6dc6d4c..cb17b154 100644 --- a/citar-capf.el +++ b/citar-capf.el @@ -25,10 +25,10 @@ (declare-function org-element-type "ext:org-element") (declare-function org-element-context "ext:org-element") ;; Declare function from citar -;; (declare-function citar--ref-completion-table "citar") ;; pending cache revisions +(declare-function citar--ref-completion-table "citar") ;; pending cache revisions ;; Define vars for capf -(defvar citar-capf--candidates (or (citar--get-candidates) +(defvar citar-capf--candidates (or (citar--ref-completion-table) (user-error "No bibliography set")) "Completion candidates for `citar-capf'.") @@ -41,7 +41,7 @@ (defun citar-capf--exit (str _status) "Return key for STR from CANDIDATES hash." (delete-char (- (length str))) - (insert (cadr (assoc str citar-capf--candidates)))) + (insert (gethash str citar-capf--candidates))) ;;;; Citar-Capf ;;;###autoload From 2af3c953729c0a9d05de186afa2f31043f427b07 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 10:25:01 -0600 Subject: [PATCH 10/78] Fix `citar-open-entry` Co-authored-by: Bruce D'Arcus --- citar.el | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/citar.el b/citar.el index 393cd283..5ee2e3ff 100644 --- a/citar.el +++ b/citar.el @@ -1014,11 +1014,7 @@ With prefix, rebuild the cache before offering candidates." "Open bibliographic entry associated with the KEY. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) - (when-let ((bibtex-files - (seq-concatenate - 'list - citar-bibliography - (citar--local-files-to-cache)))) + (when-let ((bibtex-files (citar--bibliography-files)) (bibtex-search-entry (car key) t nil t))) ;;;###autoload From 04f7e7704b9382a93ce157ba680ce6b738982289 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 13:33:53 -0600 Subject: [PATCH 11/78] Fix missing closing paren --- citar.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/citar.el b/citar.el index 5ee2e3ff..09a388f6 100644 --- a/citar.el +++ b/citar.el @@ -1014,7 +1014,7 @@ With prefix, rebuild the cache before offering candidates." "Open bibliographic entry associated with the KEY. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) - (when-let ((bibtex-files (citar--bibliography-files)) + (when-let ((bibtex-files (citar--bibliography-files))) (bibtex-search-entry (car key) t nil t))) ;;;###autoload From 64abfdde887eb502c59e93d2e60bc858d9cec587 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 09:41:48 -0600 Subject: [PATCH 12/78] Improve the performance of formatting functions. Also add tests for `citar-format--star-widths` --- citar-cache.el | 26 ++++-- citar-format.el | 192 +++++++++++++++----------------------- citar.el | 48 ++++++---- test/citar-format-test.el | 59 ++++++++++++ 4 files changed, 183 insertions(+), 142 deletions(-) create mode 100644 test/citar-format-test.el diff --git a/citar-cache.el b/citar-cache.el index 784ba4b0..ef3839ab 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -31,6 +31,7 @@ (declare-function citar--get-template "citar") (declare-function citar--fields-to-parse "citar") +(defvar citar-ellipsis) ;;; Variables: @@ -189,7 +190,7 @@ modified since the last time BIB was updated." (buffer-hash)))) ;; TODO Also check file size and modification time before hashing? ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` - (unless (or force (equal newhash (citar-cache--bibliography-hash bib))) + (when (or force (not (equal newhash (citar-cache--bibliography-hash bib)))) ;; Update entries (clrhash entries) (parsebib-parse filename :entries entries :fields (citar--fields-to-parse)) @@ -201,13 +202,26 @@ modified since the last time BIB was updated." (defun citar-cache--preformat-bibliography (bib) "Updated pre-formatted strings in BIB." (let* ((entries (citar-cache--bibliography-entries bib)) - (fmtstr (citar-cache--bibliography-format-string bib)) - (preformat (citar-format--preformat fmtstr :hide-elided t)) + (formatstr (citar-cache--bibliography-format-string bib)) + (fieldspecs (citar-format--parse formatstr)) (preformatted (citar-cache--bibliography-preformatted bib))) (clrhash preformatted) - (maphash (lambda (citekey entry) - (puthash citekey (funcall preformat entry) preformatted)) - entries))) + (maphash + (lambda (citekey entry) + (let* ((preformat (citar-format--preformat fieldspecs entry + t citar-ellipsis)) + ;; CSL-JSOM lets citekey be an arbitrary string. Quote it if... + (keyquoted (if (or (string-empty-p citekey) ; ... it's empty, + (= ?\" (aref citekey 0)) ; ... starts with ", + (cl-find ?\s citekey)) ; ... or has a space + (prin1-to-string citekey) + citekey)) + (prefix (propertize (concat keyquoted (when (cdr preformat) " ")) + 'invisible t))) + (setcdr preformat (cons (concat prefix (cadr preformat)) + (cddr preformat))) + (puthash citekey preformat preformatted))) + entries))) ;;; Utility functions: diff --git a/citar-format.el b/citar-format.el index 73f8eabe..5495b929 100644 --- a/citar-format.el +++ b/citar-format.el @@ -27,64 +27,63 @@ (require 'cl-lib)) (declare-function citar--display-value "citar") -(defvar citar-ellipsis) ;;; Formatting bibliography entries -(cl-defun citar-format--entry (format-string entry &key width hide-elided - (ellipsis citar-ellipsis)) +(cl-defun citar-format--entry (format-string entry &optional width + &key hide-elided ellipsis) "Format ENTRY according to FORMAT-STRING." - (cl-flet ((getwidth (fieldspec) - (unless (stringp fieldspec) - (plist-get (car fieldspec) :width))) - (fmtfield (fieldspec) - (citar-format--fieldspec fieldspec entry - :hide-elided hide-elided - :ellipsis ellipsis))) - (let* ((fieldspecs (citar-format--parse format-string)) - (widths (mapcar #'getwidth fieldspecs)) - (strings (mapcar #'fmtfield fieldspecs))) - (citar-format--star-widths widths strings :width width - :hide-elided hide-elided :ellipsis ellipsis)))) + (let* ((fieldspecs (citar-format--parse format-string)) + (preform (citar-format--preformat fieldspecs entry + hide-elided ellipsis))) + (if width + (citar-format--star-widths (- width (car preform)) (cdr preform) + hide-elided ellipsis) + (apply #'concat (cdr preform))))) ;;; Pre-formatting bibliography entries -(cl-defun citar-format--preformat (format-string &key hide-elided - (ellipsis citar-ellipsis)) - "Preformat according to FORMAT-STRING. -See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." - (let ((fieldgroups (citar-format--preformat-parse format-string))) - (lambda (entry) - (cl-flet ((fmtfield (fieldspec) - (citar-format--fieldspec fieldspec entry - :hide-elided hide-elided - :ellipsis ellipsis))) - (mapcar (lambda (groupspec) - (mapconcat #'fmtfield (cdr groupspec) "")) - fieldgroups))))) - - -(cl-defun citar-format--preformatted (format-string &key width hide-elided - (ellipsis citar-ellipsis)) - "Fit pre-formatted strings to WIDTH according to FORMAT-STRING. -See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." - (let* ((fieldgroups (citar-format--preformat-parse format-string)) - (widths (mapcar (lambda (groupspec) - (plist-get (car groupspec) :width)) - fieldgroups))) - (lambda (preformatted) - (citar-format--star-widths widths preformatted :width width - :hide-elided hide-elided :ellipsis ellipsis)))) +(defun citar-format--preformat (fieldspecs entry hide-elided ellipsis) + (let ((preformatted nil) + (fields "") + (width 0)) + (dolist (fieldspec fieldspecs) + (pcase fieldspec + ((pred stringp) + (cl-callf concat fields fieldspec) + (cl-incf width (string-width fieldspec))) + (`(,props . ,fieldnames) + (let* ((fieldwidth (plist-get props :width)) + (textprops (plist-get props :text-properties)) + (value (citar--display-value fieldnames entry)) + (display (citar-format--string value + :width fieldwidth + :text-properties textprops + :hide-elided hide-elided + :ellipsis ellipsis))) + (cond + ((eq '* fieldwidth) + (push fields preformatted) + (setq fields "") + (push display preformatted)) + (t + (cl-callf concat fields display) + (cl-incf width (if (numberp fieldwidth) + fieldwidth + (string-width value))))))))) + (unless (string-empty-p fields) + (push fields preformatted)) + (cons width (nreverse preformatted)))) ;;; Internal implementation functions -(cl-defun citar-format--fieldspec (fieldspec entry &key hide-elided ellipsis) +(defun citar-format--fieldspec (fieldspec entry hide-elided ellipsis) "Format FIELDSPEC using information from ENTRY. See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." (if (stringp fieldspec) @@ -97,7 +96,7 @@ See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." fmtprops)))) -(cl-defun citar-format--string (string +(cl-defsubst citar-format--string (string &key width text-properties hide-elided ellipsis) "Truncate STRING to WIDTH and apply TEXT-PROPERTIES. If HIDE-ELIDED is non-nil, the truncated part of STRING is @@ -110,38 +109,38 @@ display instead of the truncated part of the text." (setq string (truncate-string-to-width string width 0 ?\s ellipsis hide-elided))) string) - -(cl-defun citar-format--star-widths (widths strings &key width - hide-elided ellipsis) - "Format STRINGS according to WIDTHS to fit WIDTH." - (if (not (and (numberp width) (cl-find '* widths))) - ;; If width is unlimited or there are no *-width fields, just join strings. - ;; We only support truncating *-width fields. - (string-join strings) - ;; Otherwise, calculate extra space available for *-width fields - (let ((usedwidth 0) (nstars 0)) - ;; For fields without width spec, add their actual width to usedwidth - (cl-mapc - (lambda (width string) - (cond ((eq '* width) (cl-incf nstars)) - ((numberp width) (cl-incf usedwidth width)) - ((null width) (cl-incf usedwidth (string-width string))))) - widths strings) - (let* ((extrawidth (max 0 (- width usedwidth))) - (starwidth (/ extrawidth nstars)) - (remainder (% extrawidth nstars)) - (starindex 0)) - (string-join - (cl-mapcar - (lambda (width string) - (if (not (eq width '*)) - string - (cl-incf starindex) - (citar-format--string - string - :width (+ starwidth (if (<= starindex remainder) 1 0)) - :hide-elided hide-elided :ellipsis ellipsis))) - widths strings)))))) +(defun citar-format--star-widths (alloc strings &optional hide-elided ellipsis) + "Concatenate STRINGS and truncate every other element to fit in ALLOC. +Use this function along with `citar-format--preformat' to fit a +formatted string to a desired display width; see +`citar-format--entry' for how to do this. + +Return a string consisting of the concatenated elements of +STRINGS. The odd-numbered elements are included as-is, while the +even-numbered elements are padded or truncated to a total width +of ALLOC, which must be an integer. All these odd-numbered +elements are allocated close-to-equal widths. + +Perform the truncation using `citar-format--string', which see +for the meaning of HIDE-ELIDED and ELLIPSIS." + (let ((nstars (/ (length strings) 2))) + (if (= 0 nstars) + (or (car strings) "") + (cl-loop + with alloc = (max 0 alloc) + with starwidth = (/ alloc nstars) + with remainder = (% alloc nstars) + with formatted = (car strings) + for (starstring following) on (cdr strings) by #'cddr + for nthstar from 1 + do (let* ((starwidth (if (> nthstar remainder) starwidth + (1+ starwidth))) + (starstring (citar-format--string + starstring + :width starwidth + :hide-elided hide-elided :ellipsis ellipsis))) + (cl-callf concat formatted starstring following)) + finally return formatted)))) ;;; Parsing format strings @@ -178,48 +177,5 @@ display instead of the truncated part of the text." (nreverse fieldspecs))) -(defun citar-format--preformat-parse (format-string) - "Parse and group FORMAT-STRING." - (let (fieldgroups group (groupwidth 0)) - (cl-flet ((newgroup () - (when group - (push (cons (when groupwidth `(:width ,groupwidth)) - (nreverse group)) - fieldgroups) - (setq group nil)) - (setq groupwidth 0))) - (dolist (fieldspec (citar-format--parse format-string)) - (let ((fieldwidth (cond - ((stringp fieldspec) (string-width fieldspec)) - ((listp fieldspec) (plist-get (car fieldspec) :width))))) - (cond - ((eq fieldwidth '*) - ;; *-width field; start a new group - (newgroup) - ;; Pre-format the field at unlimited width by setting :width to nil - (cl-callf plist-put (car fieldspec) :width nil) - ;; Add the field in its own pre-format group with :width * - (push fieldspec group) - (setq groupwidth '*) - (newgroup)) - ((numberp fieldwidth) - ;; Fixed-length field; start new group if needed - (unless (numberp groupwidth) - (newgroup)) - ;; Add field to group and increment group width - (push fieldspec group) - (cl-incf groupwidth fieldwidth)) - (t - ;; Unknown-length field; start new group if needed - (unless (null groupwidth) - (newgroup)) - ;; Add field to group; group width is now unknown - (push fieldspec group) - (setq groupwidth nil))))) - ;; Add any remaining fields to group - (newgroup)) - (nreverse fieldgroups))) - - (provide 'citar-format) ;;; citar-format.el ends here diff --git a/citar.el b/citar.el index 09a388f6..8ae94a35 100644 --- a/citar.el +++ b/citar.el @@ -641,13 +641,31 @@ Return nil if there are no bibliography files or no entries." (maphash (lambda (citekey preform) (let* ((entry (gethash citekey entries)) - (cand (concat (string-trim-right (funcall format preform)) - (when (funcall hasnotep citekey entry) hasnotetag) - (when (funcall hasfilep citekey entry) hasfiletag)))) + (starswidth (- width (car preform))) + (strings (cdr preform)) + (display (citar-format--star-widths + starswidth strings t citar-ellipsis)) + ;; (hasfile (and hasfilep (funcall hasfilep citekey entry))) + (hasnote (and hasnotep (funcall hasnotep citekey entry))) + (hasfile t) + ;; (hasnote t) + (cand (if (not (or hasfile hasnote)) display + (concat display + (when hasnote hasnotetag) + (when hasfile hasfiletag))))) (puthash cand citekey completions))) preformatted) completions))) +(defun citar--extract-candidate-citekey (candidate) + "Extract the citation key from string CANDIDATE." + (unless (string-empty-p candidate) + (if (= ?\" (aref candidate 0)) + (read candidate) + (substring-no-properties candidate 0 (cl-position ?\s candidate))))) + +;;; Major-mode functions + (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. If no function is found matching KEY for the current major mode, @@ -667,7 +685,7 @@ return DEFAULT." If no function is found, the DEFAULT function is called." (apply (citar--get-major-mode-function key default) args)) -;; Data access functions +;;; Data access functions (defun citar--get-entry (key) "Return entry for KEY, as an association list." @@ -722,10 +740,8 @@ personal names of the form \"family, given\"." (defun citar--fields-for-format (template) "Return list of fields for TEMPLATE." - (let* ((regexp "\\(?:\\`\\|}\\|:\\)[^{]*\\(?:\\${\\|\\'\\)\\|[[:space:]]+")) - ;; The readable version of regexp is: - ;; (rx (or (seq (or bos "}" ":") (0+ (not "{")) (or "${" eos)) (1+ space))) - (split-string template regexp t))) + (mapcan (lambda (fieldspec) (when (consp fieldspec) (cdr fieldspec))) + (citar-format--parse template))) (defun citar--fields-in-formats () "Find the fields to mentioned in the templates." @@ -737,11 +753,10 @@ personal names of the form \"family, given\"." (defun citar--fields-to-parse () "Determine the fields to parse from the template." - (seq-concatenate - 'list - (citar--fields-in-formats) - (list citar-file-variable) - citar-additional-fields)) + (delete-dups (append (citar--fields-in-formats) + (when citar-file-variable + (list citar-file-variable)) + citar-additional-fields))) (defun citar-has-file () "Return predicate testing whether entry has associated files. @@ -869,10 +884,7 @@ repeatedly." (defun citar--reference-transformer (type target) "Look up key for a citar-reference TYPE and TARGET." - (cons type (or (cadr (assoc target - (with-current-buffer (embark--target-buffer) - ;; FIX how? - (citar--get-candidates))))))) + (cons type (citar--extract-candidate-citekey target))) (defun citar--embark-selected () "Return selected candidates from `citar--select-multiple' for embark." @@ -1028,7 +1040,7 @@ With prefix, rebuild the cache before offering candidates." (defun citar--insert-bibtex (key) "Insert the bibtex entry for KEY at point." (let* ((bibtex-files - (seq-concatenate 'list citar-bibliography (citar--local-files-to-cache))) + (citar--bibliography-files)) (entry (with-temp-buffer (bibtex-set-dialect) diff --git a/test/citar-format-test.el b/test/citar-format-test.el new file mode 100644 index 00000000..ddeba9b9 --- /dev/null +++ b/test/citar-format-test.el @@ -0,0 +1,59 @@ +;;; citar-format-test.el --- Tests for citar-format.el -*- lexical-binding: t; -*- + +;;; Commentary: + +;; + +;;; Code: + +(require 'ert) +(require 'seq) +(require 'citar-format) + +(ert-deftest citar-format-test--star-widths () + "Test `citar-format--star-widths`." + + (should (string-empty-p (citar-format--star-widths 80 nil))) + + ;; For single string, return the original string; not a copy + (let ((strings '("foo"))) + (should (eq (car strings) (citar-format--star-widths 80 strings)))) + + (let ((strings '("foo" "bar" "baz"))) + (should (equal "foobaz" (citar-format--star-widths 0 strings))) + (should (equal "foobabaz" (citar-format--star-widths 2 strings))) + (should (equal "foob…baz" (citar-format--star-widths 2 strings nil "…"))) + (should (equal "foobarbaz" (citar-format--star-widths 3 strings))) + (should (equal "foobar baz" (citar-format--star-widths 4 strings))) + + ;; When hide-elided is t, the actual string contents should be equal + (cl-loop for w from 0 to 3 + do (should (equal "foobarbaz" (citar-format--star-widths w strings t)))) + ;; ...unless the allocated width is greater than the string length + (should (equal "foobar baz" (citar-format--star-widths 4 strings))) + + ;; When hide-elided is t, the hidden text should have the 'display "" + ;; property. N.B. equal-including-properties is slightly broken; see + ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=6581 + (should (ert-equal-including-properties #("foobarbaz" 5 6 (display "")) + (citar-format--star-widths 2 strings t))) + + ;; Test with ellipsis + (should (ert-equal-including-properties #("foobarbaz" 4 6 (display "…")) + (citar-format--star-widths 2 strings t "…")))) + + (let ((strings '("foo" "bar" "baz" "qux"))) + (should (equal "foobaz" (citar-format--star-widths 0 strings))) + (should (equal "foobbaz" (citar-format--star-widths 1 strings))) + (should (equal "foobbazq" (citar-format--star-widths 2 strings))) + (should (equal "foobabazq" (citar-format--star-widths 3 strings))) + (should (equal "foobabazqu" (citar-format--star-widths 4 strings))) + + ;; Test with ellipsis + (should (equal "foob…baz…" (citar-format--star-widths 3 strings nil "…"))) + (should (ert-equal-including-properties + #("foobarbazqux" 4 6 (display "…") 9 12 (display "…")) + (citar-format--star-widths 3 strings t "…"))))) + +(provide 'citar-format-test) +;;; citar-format-test.el ends here From 2d4e19b0002fe078e1a7e1c9d1e952d51dcc4114 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 17 Jun 2022 16:10:10 -0600 Subject: [PATCH 13/78] Use `map-merge` to merge hash tables --- citar-cache.el | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/citar-cache.el b/citar-cache.el index ef3839ab..4be85b8e 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -27,6 +27,7 @@ (require 'cl-lib)) (require 'parsebib) (require 'citar-format) +(require 'map) (declare-function citar--get-template "citar") (declare-function citar--fields-to-parse "citar") @@ -110,12 +111,12 @@ element of FILENAMES." nil)) (defun citar-cache--entries (bibs) - (citar-cache--merge-hash-tables - (mapcar #'citar-cache--bibliography-entries bibs))) + (apply #'map-merge '(hash-table :test equal) + (nreverse (mapcar #'citar-cache--bibliography-entries bibs)))) (defun citar-cache--preformatted (bibs) - (citar-cache--merge-hash-tables - (mapcar #'citar-cache--bibliography-preformatted bibs))) + (apply #'map-merge '(hash-table :test equal) + (nreverse (mapcar #'citar-cache--bibliography-preformatted bibs)))) ;;; Creating and deleting bibliography caches @@ -238,16 +239,5 @@ Otherwise, return the buffer object whose name is BUFFER." (t (get-buffer buffer)))) -(defun citar-cache--merge-hash-tables (hash-tables) - "Merge hash tables in HASH-TABLES." - (when-let ((hash-tables (reverse hash-tables)) - (first (pop hash-tables))) - (if (null hash-tables) - first - (let ((result (copy-hash-table first))) - (dolist (table hash-tables result) - (maphash (lambda (key entry) (puthash key entry result)) table)))))) - - (provide 'citar-cache) ;;; citar-cache.el ends here From 86b1dba96d6f8006297d4d5c6cfc558413dd8518 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 17 Jun 2022 18:20:15 -0600 Subject: [PATCH 14/78] WIP: Speed up has-file indicator --- citar-file.el | 57 ++++++---------------- citar.el | 131 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 80 deletions(-) diff --git a/citar-file.el b/citar-file.el index 30d2d717..97fbb240 100644 --- a/citar-file.el +++ b/citar-file.el @@ -232,44 +232,6 @@ need to scan the contents of DIRS in this case." (puthash key (nreverse filelist) files)) files)))) -(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: - -- 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`. - -- 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 &optional entry) - (let* ((nentry (or entry (citar--get-entry key))) - (xref (citar--get-value "crossref" nentry)) - (cached (if (and xref - (not (eq 'unknown (gethash xref files 'unknown)))) - (gethash xref files 'unknown) - (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 nentry entry-field dirs extensions)) - files)))))) - (defun citar-file--files-for-entry (key entry dirs extensions) "Find files related to bibliography item KEY with metadata ENTRY. See `citar-file--files-for-multiple-entries` for details on DIRS, @@ -326,10 +288,21 @@ of files found in two ways: nil 0 nil file))) -(defun citar-file-has-notes () - "Return a predicate testing whether a reference has associated notes." - (citar-file--has-file citar-notes-paths - citar-file-note-extensions)) +(defun citar-file-has-library-files (&optional _entries) + "Return predicate testing whether cite key has library files." + (let ((files (citar-file--directory-files + citar-library-paths nil citar-library-file-extensions + citar-file-additional-files-separator))) + (lambda (key) + (gethash key files)))) + +(defun citar-file-has-notes (&optional _entries) + "Return predicate testing whether cite key has associated notes." + (let ((files (citar-file--directory-files + citar-notes-paths nil citar-file-note-extensions + citar-file-additional-files-separator))) + (lambda (key) + (gethash key files)))) (defun citar-file--open-note (key entry) "Open a note file from KEY and ENTRY." diff --git a/citar.el b/citar.el index 8ae94a35..a0aaabc6 100644 --- a/citar.el +++ b/citar.el @@ -40,6 +40,7 @@ (require 'cl-lib) (require 'subr-x)) (require 'seq) +(require 'map) (require 'browse-url) (require 'citar-cache) (require 'citar-format) @@ -109,6 +110,12 @@ :group 'citar :type '(repeat file)) +(defcustom citar-has-file-functions '(citar-has-file-field + citar-file-has-library-files) + "List of functions to test if an entry has associated files." + :group 'citar + :type '(repeat function)) + (defcustom citar-library-paths nil "A list of files paths for related PDFs, etc." :group 'citar @@ -129,7 +136,18 @@ When nil, the function will not filter the list of files." :group 'citar :type '(repeat directory)) -(defcustom citar-additional-fields '("doi" "url" "crossref") +(defcustom citar-crossref-variable "crossref" + "The bibliography field to look for cross-referenced entries. + +When non-nil, find associated files and notes not only in the +original entry, but also in entries specified in the field named +by this variable." + :group 'citar + :type '(choice (const "crossref") + (string :tag "Field name") + (const :tag "Ignore cross-references" nil))) + +(defcustom citar-additional-fields '("doi" "url") "A list of fields to add to parsed data. By default, citar filters parsed data based on the fields @@ -619,42 +637,38 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) -(defun citar--ref-completion-table () +(cl-defun citar--ref-completion-table (&optional (bibs (citar--bibliographies)) + (entries (citar-cache--entries bibs))) "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and title of the reference. Values are the cite keys. Return nil if there are no bibliography files or no entries." ;; Populate bibliography cache. - (when-let ((bibs (citar--bibliographies))) - (let* ((entries (citar-cache--entries bibs)) - (preformatted (citar-cache--preformatted bibs)) - (fmtstr (citar--get-template 'completion)) - (hasnotep (citar-has-note)) - (hasfilep (citar-has-file)) + (when bibs + (let* ((preformatted (citar-cache--preformatted bibs)) + (hasnotep (citar-has-notes-for-entries entries)) + (hasfilep (citar-has-files-for-entries entries)) (hasnotetag (propertize " has:notes" 'invisible t)) (hasfiletag (propertize " has:files" 'invisible t)) (symbolswidth (string-width (citar--symbols-string t t t))) (width (- (frame-width) symbolswidth 2)) - (format (citar-format--preformatted - fmtstr :width width :hide-elided t :ellipsis citar-ellipsis)) - (completions (make-hash-table :test 'equal))) + (completions (make-hash-table :test 'equal :size (hash-table-count entries)))) (maphash - (lambda (citekey preform) - (let* ((entry (gethash citekey entries)) - (starswidth (- width (car preform))) - (strings (cdr preform)) + (lambda (citekey _entry) + (let* ((hasfile (and hasfilep (funcall hasfilep citekey))) + (hasnote (and hasnotep (funcall hasnotep citekey))) + (preform (or (gethash citekey preformatted) + (error "No preformatted candidate string: %s" citekey))) (display (citar-format--star-widths - starswidth strings t citar-ellipsis)) - ;; (hasfile (and hasfilep (funcall hasfilep citekey entry))) - (hasnote (and hasnotep (funcall hasnotep citekey entry))) - (hasfile t) - ;; (hasnote t) - (cand (if (not (or hasfile hasnote)) display - (concat display - (when hasnote hasnotetag) - (when hasfile hasfiletag))))) - (puthash cand citekey completions))) - preformatted) + (- width (car preform)) (cdr preform) + t citar-ellipsis)) + (tagged (if (not (or hasfile hasnote)) + display + (concat display + (when hasnote hasnotetag) + (when hasfile hasfiletag))))) + (puthash tagged citekey completions))) + entries) completions))) (defun citar--extract-candidate-citekey (candidate) @@ -756,9 +770,18 @@ personal names of the form \"family, given\"." (delete-dups (append (citar--fields-in-formats) (when citar-file-variable (list citar-file-variable)) + (when citar-crossref-variable + (list citar-crossref-variable)) citar-additional-fields))) -(defun citar-has-file () +(defun citar-has-file-field (entries) + "Return predicate to test if bibliography entry has a file field." + (when-let ((fieldname citar-file-variable)) + (lambda (key) + (when-let ((entry (map-elt entries key))) + (citar--get-value fieldname entry))))) + +(defun citar-has-file-p (key &optional entry) "Return predicate testing whether entry has associated files. Return a function that takes arguments KEY and ENTRY and returns @@ -769,11 +792,11 @@ non-nil when the entry has associated files, either in Note: for performance reasons, this function should be called once per command; the function it returns can be called repeatedly." - (citar-file--has-file citar-library-paths - citar-library-file-extensions - citar-file-variable)) + (when-let ((entry (or entry (citar--get-entry entry))) + (hasfilep (citar-has-files-for-entries '((key . entry))))) + (funcall hasfilep key))) -(defun citar-has-note () +(defun citar-has-note-p (key &optional entry) "Return predicate testing whether entry has associated notes. Return a function that takes arguments KEY and ENTRY and returns @@ -782,13 +805,45 @@ 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." - ;; Call each function in `citar-has-note-functions` to get a list of predicates - (let ((preds (mapcar #'funcall citar-has-note-functions))) - ;; Return a predicate that checks if `citekey` and `entry` have a note - (lambda (citekey &optional entry) - (let ((nentry (or entry (citar--get-entry citekey)))) - ;; Call each predicate with `citekey` and `entry`; return the first non-nil result - (seq-some (lambda (pred) (funcall pred citekey nentry)) preds))))) + (when-let ((entry (or entry (citar--get-entry entry))) + (hasnotep (citar-has-notes-for-entries '((key . entry))))) + (funcall hasnotep key))) + +(defun citar-has-notes-for-entries (entries) + (citar--has-resources-for-entries citar-has-note-functions entries)) + +(defun citar-has-files-for-entries (entries) + (citar--has-resources-for-entries citar-has-file-functions entries)) + +(defun citar--has-resources-for-entries (functions entries) + "Return predicate combining results of calling FUNCTIONS. + +FUNCTIONS should be a list of functions, each of which returns a +predicate function that takes KEY and ENTRY arguments. Run each +function in the list, and return a predicate that is the logical +or of all these predicates. + +The FUNCTIONS may also return nil, which is treated as an +always-false predicate and ignored. If there is only one non-nil +predicate, return it." + (when-let ((predicates (delq nil (mapcar (lambda (fn) + (funcall fn entries)) + functions)))) + (let ((hasresourcep (if (null (cdr predicates)) + ;; optimization for single predicate; just use it directly + (car predicates) + ;; otherwise, call all predicates until one returns non-nil + (lambda (citekey) + (seq-some (lambda (predicate) + (funcall predicate citekey)) + predicates))))) + (if-let ((crossref citar-crossref-variable)) + (lambda (citekey) + (or (funcall hasresourcep citekey) + (when-let ((entry (map-elt entries citekey)) + (crossrefkey (citar--get-value crossref entry))) + (funcall hasresourcep crossrefkey)))) + hasresourcep)))) (defun citar--ref-affix (cands) "Add affixation prefix to CANDS." From f6a0d96c868f17bf6b5692f7e2b5c5da2899f4bf Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 13:04:42 -0400 Subject: [PATCH 15/78] Fix compilation errors, indenting --- citar-citeproc.el | 9 +++------ citar-filenotify.el | 6 ++---- citar-format.el | 3 +-- citar-markdown.el | 2 +- citar-org.el | 3 ++- citar.el | 12 +++++------- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/citar-citeproc.el b/citar-citeproc.el index 3a6acab4..bb44e55d 100644 --- a/citar-citeproc.el +++ b/citar-citeproc.el @@ -96,8 +96,8 @@ accepted.") (setq citar-citeproc-csl-style file))) ;;;###autoload -(defun citar-citeproc-format-reference (keys-entries) - "Return formatted reference(s) for KEYS-ENTRIES via `citeproc-el`. +(defun citar-citeproc-format-reference (keys) + "Return formatted reference(s) for KEYS via `citeproc-el`. Formatting follows CSL style set in `citar-citeproc-csl-style`. With prefix-argument, select CSL style." (when (or (eq citar-citeproc-csl-style nil) @@ -108,10 +108,7 @@ With prefix-argument, select CSL style." (let* ((style (if (string-match-p "/" citar-citeproc-csl-style) citar-citeproc-csl-style (expand-file-name citar-citeproc-csl-style citar-citeproc-csl-styles-dir))) - (keys (citar--extract-keys keys-entries)) - (bibs (flatten-list - (list citar-bibliography - (citar--major-mode-function 'local-bib-files #'ignore)))) + (bibs (citar--bibliography-files)) (proc (citeproc-create style (citeproc-hash-itemgetter-from-any bibs) (citeproc-locale-getter-from-dir citar-citeproc-csl-locales-dir) diff --git a/citar-filenotify.el b/citar-filenotify.el index d2ffc431..31beceb6 100644 --- a/citar-filenotify.el +++ b/citar-filenotify.el @@ -29,7 +29,7 @@ (require 'files) (require 'citar) -(declare-function citar-refresh "citar") +;(declare-function citar-refresh "citar") (declare-function citar--local-files-to-cache "citar") (declare-function citar-file--normalize-paths "citar-file") (declare-function reftex-access-scan-info "ext:reftex") @@ -88,9 +88,7 @@ CHANGE refers to the notify argument." ((or 'created 'deleted 'renamed) (if (member (nth 2 change) - (seq-concatenate 'list - (citar-file--normalize-paths citar-bibliography) - (citar--local-files-to-cache))) + (citar--bibliography-files)) (citar-filenotify-refresh scope) (funcall func scope))))) diff --git a/citar-format.el b/citar-format.el index 5495b929..b0a9747f 100644 --- a/citar-format.el +++ b/citar-format.el @@ -43,7 +43,6 @@ hide-elided ellipsis) (apply #'concat (cdr preform))))) - ;;; Pre-formatting bibliography entries @@ -97,7 +96,7 @@ See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." (cl-defsubst citar-format--string (string - &key width text-properties hide-elided ellipsis) + &key width text-properties hide-elided ellipsis) "Truncate STRING to WIDTH and apply TEXT-PROPERTIES. If HIDE-ELIDED is non-nil, the truncated part of STRING is covered by a display property that makes it invisible, instead of diff --git a/citar-markdown.el b/citar-markdown.el index 8d706e08..7b634102 100644 --- a/citar-markdown.el +++ b/citar-markdown.el @@ -90,7 +90,7 @@ If INVERT-PROMPT is non-nil, invert the meaning of "Prompt for keys and call `citar-markdown-insert-citation. With ARG non-nil, rebuild the cache before offering candidates." (citar-markdown-insert-citation - (citar--extract-keys (citar-select-refs :rebuild-cache arg)))) + (citar-select-refs :rebuild-cache arg))) ;;;###autoload (defun citar-markdown-key-at-point () diff --git a/citar-org.el b/citar-org.el index e76979ff..a6b93b7a 100644 --- a/citar-org.el +++ b/citar-org.el @@ -43,6 +43,7 @@ (make-obsolete 'citar-org-id-get-create 'citar-org--id-get-create "1.0") +(declare-function citar-format--entry "citar-format") (declare-function org-open-at-point "org") (declare-function org-element-property "org-element") (declare-function org-element-type "org-element") @@ -163,7 +164,7 @@ With PROC list, limit to specific processor(s)." (defun citar-org-select-key (&optional multiple) "Return a list of keys when MULTIPLE, or else a key string." (if multiple - (citar--extract-keys (citar-select-refs)) + (citar-select-refs) (car (citar-select-ref)))) ;;;###autoload diff --git a/citar.el b/citar.el index a0aaabc6..ffd23082 100644 --- a/citar.el +++ b/citar.el @@ -45,7 +45,6 @@ (require 'citar-cache) (require 'citar-format) (require 'citar-file) -(require 'crm) ;;; pre-1.0 API cleanup @@ -457,7 +456,7 @@ current buffer and global bibliographies." (list citar-bibliography)) (with-current-buffer buffer (citar--major-mode-function 'local-bib-files #'ignore)))) - (or buffers (list (current-buffer) 'global))))) + (or buffers (list (current-buffer) 'global))))) (defun citar--bibliographies (&rest buffers) "Return bibliographies for BUFFERS." @@ -991,7 +990,6 @@ predicate, return it." '((multi-category . citar--open-multi) (file . citar-file-open) (url . browse-url))) - (key-entry-alist (citar--ensure-entries keys-entries)) (files (citar-file--files-for-multiple-entries keys @@ -1046,8 +1044,8 @@ For use with `embark-act-all'." (message "No associated file")))) ;;;###autoload -(defun citar-open-library-file (key-entry) - "Open library file associated with the KEY-ENTRY. +(defun citar-open-library-file (key) + "Open library file associated with the KEY. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) @@ -1218,7 +1216,7 @@ With prefix, rebuild the cache before offering candidates." (when (and citar-library-paths (stringp citar-library-paths)) (error "Make sure 'citar-library-paths' is a list of paths")) - (citar--library-file-action key-entry 'attach))) + (citar--library-file-action key 'attach))) (defun citar--add-file-to-library (key) "Add a file to the library for KEY. @@ -1258,7 +1256,7 @@ The FILE can be added either from an open buffer, a file, or a URL." (interactive (list (citar-select-ref :rebuild-cache current-prefix-arg))) - (citar--add-file-to-library (car key-entry))) + (citar--add-file-to-library key)) ;;;###autoload (defun citar-run-default-action (keys) From 62a6c493c6a34f2add5bfa5f5a1b2c56eb3d25d5 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sat, 18 Jun 2022 20:35:55 -0600 Subject: [PATCH 16/78] Small tweaks to the Eldev and GitHub CI configuration Add advice in Eldev file to disable the `with-eval-after-load` warning in package-lint. If it produces any other warnings, however, then fail the CI check. --- .dir-locals.el | 20 +++++++++++--------- .github/workflows/check.yml | 2 +- Eldev | 12 +++++++++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.dir-locals.el b/.dir-locals.el index 57444ef8..729ba419 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,9 +1,11 @@ -((emacs-lisp-mode - (fill-column . 110) - (indent-tabs-mode . nil) - (elisp-lint-indent-specs . ((describe . 1) - (it . 1) - (thread-first . 0) - (cl-flet . 1) - (sentence-end-double-space . nil) - (cl-flet* . 1))))) +;;; Directory Local Variables +;;; For more information see (info "(emacs) Directory Variables") + +((emacs-lisp-mode . ((sentence-end-double-space . nil) + (fill-column . 110) + (indent-tabs-mode . nil) + (elisp-lint-indent-specs . ((describe . 1) + (it . 1) + (thread-first . 0) + (cl-flet . 1) + (cl-flet* . 1)))))) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7c689ce1..4aaf39ec 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -64,4 +64,4 @@ jobs: - name: Lint package metadata if: ${{ matrix.action == 'lint' }} run: | - eldev --color lint package || true + eldev --color lint package diff --git a/Eldev b/Eldev index 70f206ce..a1223d7f 100755 --- a/Eldev +++ b/Eldev @@ -11,15 +11,17 @@ (setf eldev-standard-excludes `(:or ,eldev-standard-excludes "./test/manual/" "./citar-capf.el")) ;; (setf eldev-test-fileset '("./test/" "!./test/manual/")) -;; (eldev-add-extra-dependencies 'test 'embark 'consult 'marginalia 'vertico 'auctex) +(eldev-add-extra-dependencies '(build test lint) 'embark 'auctex) ;; allow to load test helpers ;; (eldev-add-loading-roots 'test "test/utils") +;;; Linting settings + ;; Tell checkdoc not to demand two spaces after a period. (setq sentence-end-double-space nil) - -(setf eldev-lint-default '(elisp)) +(setq eldev-lint-default '(elisp)) +(setq eldev-lint-stop-mode 'linter) (with-eval-after-load 'elisp-lint ;; Used eldev lint package | checkdoc @@ -28,3 +30,7 @@ ;; Emacs 29 snapshot has new indentation convention for cl-letf (when (> emacs-major-version 28) (push "indent" elisp-lint-ignored-validators))) + +;; Currently, package-lint has no other way of ignoring checks. +;; See https://github.com/purcell/package-lint/issues/125 +(advice-add #'package-lint--check-eval-after-load :override #'ignore) From e4c79adbf5003c64d56a7a9319a55c5d7dd69690 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 7 Jun 2022 10:27:51 -0400 Subject: [PATCH 17/78] refactor: split caches, make API key-focused Sorry for this breaking change, but I wanted to get the foundations right before tagging 1.0. This completely restructures the core of citar to borrow some code and ideas from the org-mode oc-basic package. In particular, it changes to using two primary caches: - bibliography - completion Both of these now use hash tables, rather than lists. Caching functionality is also changed, and the API now focuses on citekeys as arguments for key functions. Finally, citar--parse-bibliography should re-parse bibliography files upon change. Fix #623 Close #627 --- CONTRIBUTING.org | 45 ++-- citar-file.el | 37 +-- citar.el | 572 +++++++++++++++++++++-------------------------- 3 files changed, 298 insertions(+), 356 deletions(-) diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org index 91e140c7..df68cb37 100644 --- a/CONTRIBUTING.org +++ b/CONTRIBUTING.org @@ -5,31 +5,49 @@ If you would like to contribute, details: -- For more signifiant potential changes, file an issue first to get feedback on the basic idea. +- For more significant potential changes, file an issue first to get feedback on the basic idea. - If you do submit a PR, follow the [[https://github.com/bbatsov/emacs-lisp-style-guide][elisp style guide]], and [[https://cbea.ms/git-commit/][these suggestions]] on git commit messages. - For working on lists and such, we primarily use the =seq= functions, and occassionally ~dolist~. +** Basic Architecture + +Citar has two primary caches, each of which store the data in hash tables: + +- bibliographic :: keys are citekeys, values are alists of entry fields +- completion :: keys are completion strings, values are citekeys + +The =citar--ref-completion-table= function returns a hash table from the bibliographic cache, and ~citar--get-entry~ and ~-citar--get-value~ provide access to those data. +Most user-accessible citar functions take an argument ~key~ or ~keys~. +Some functions also take an ~entry~ argument, and ~citar--get-value~ takes either. +When using these functions, you should keep in mind that unless you pass an entry alist to ~citar--get-value~, and instead use a key, each call to that function will query the cache. +This, therefore, is a better pattern to use: + +#+begin_src emacs-lisp + +(let* ((entry (citar--get-entry key)) + (title (citar--get-value entry "title"))) + (message title)) + +#+end_src + + ** Extending citar -Most user-accessible citar functions take an argument ~key-entry~ or ~keys-entries~. -These expect, respectively, a cons cell of a citation key (a string like "SmithWritingHistory1987") and the corresponding bibliography entry for that citation, or a list of such cons cells. -If you wish to extend citar at the user-action level, perhaps by adding a function to one of the embark keymaps, you will find it easiest to reproduce this pattern. -If you need to build the cons cells manually, this can be accomplished via ~citar--get-entry~. -So, for example, to insert the annotations from a pdf into a buffer, the following pair of functions might be used: +You can use ~citar-select-ref~ or ~citar-select-refs~ to write custom commands. +An example: #+begin_src emacs-lisp -(defun my/citar-insert-annots (keys-entries) +(defun my/citar-insert-annots (keys) "insert annotations as org text from KEYS-ENTRIES" - (interactive (list (citar-select-refs - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-refs))) (let* ((files - (seq-mapcat (lambda (key-entry) + (seq-mapcat (lambda (key) (citar-file--files-for-entry - (car key-entry) (cdr key-entry) + key (citar--get-entry key) '("/") '("pdf"))) - keys-entries )) + keys )) (output (seq-map (lambda (file) (pdf-annot-markups-as-org-text ;; you'll still need to write this function! @@ -44,8 +62,7 @@ So, for example, to insert the annotations from a pdf into a buffer, the followi (defun my/independent-insert-annots (key) "helper function to insert annotations without the bibtex-actins apparatus" - (let ((key-entry (cons key (citar--get-entry key)))) - (my/citar-insert-annots (list key-entry)))) + (my/citar-insert-annots (list key))) #+end_src diff --git a/citar-file.el b/citar-file.el index 9c8e88fc..75e0dc3f 100644 --- a/citar-file.el +++ b/citar-file.el @@ -233,31 +233,6 @@ need to scan the contents of DIRS in this case." (puthash key (nreverse filelist) files)) files)))) -(defun citar-file--has-file-notes-hash () - "Return a hash of keys and file paths for notes." - (citar-file--directory-files - citar-notes-paths nil citar-file-note-extensions - citar-file-additional-files-separator)) - -(defun citar-file--has-library-files-hash () - "Return a hash of keys and file paths for library files." - (citar-file--directory-files - citar-library-paths nil citar-library-file-extensions - citar-file-additional-files-separator)) - -(defun citar-file--keys-with-file-notes () - "Return a list of keys with file notes." - (hash-table-keys (citar-file--has-file-notes-hash))) - -(defun citar-file--keys-with-library-files () - "Return a list of keys with file notes." - (hash-table-keys (citar-file--has-library-files-hash))) - -(defun citar-file-has-notes () - "Return a predicate testing whether a reference has associated notes." - (citar-file--has-file citar-notes-paths - citar-file-note-extensions)) - (defun citar-file--has-file (dirs extensions &optional entry-field) "Return predicate testing whether a key and entry have associated files. @@ -277,8 +252,9 @@ 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) - (let* ((xref (citar--get-value "crossref" entry)) + (lambda (key &optional entry) + (let* ((nentry (or entry (citar--get-entry key))) + (xref (citar--get-value "crossref" nentry)) (cached (if (and xref (not (eq 'unknown (gethash xref files 'unknown)))) (gethash xref files 'unknown) @@ -292,7 +268,7 @@ repeatedly." (puthash key (seq-some #'file-exists-p - (citar-file--parse-file-field entry entry-field dirs extensions)) + (citar-file--parse-file-field nentry entry-field dirs extensions)) files)))))) (defun citar-file--files-for-entry (key entry dirs extensions) @@ -351,6 +327,11 @@ of files found in two ways: nil 0 nil file))) +(defun citar-file-has-notes () + "Return a predicate testing whether a reference has associated notes." + (citar-file--has-file citar-notes-paths + citar-file-note-extensions)) + (defun citar-file--open-note (key entry) "Open a note file from KEY and ENTRY." (if-let* ((file (citar-file--get-note-filename key diff --git a/citar.el b/citar.el index 8260ae39..572c4bb8 100644 --- a/citar.el +++ b/citar.el @@ -237,15 +237,17 @@ If nil, single resources will open without prompting." (defcustom citar-open-note-functions '(citar-file--open-note) "List of functions to open a note." + ;; REVIEW change to key only arg? :group 'citar :type '(function)) (defcustom citar-has-note-functions - '(citar-file-has-notes) + '(citar--has-file-notes) "Functions used for displaying note indicators. -Such functions must take arguments KEY and ENTRY and return -non-nil when the reference has associated notes." +Such functions must take KEY and return non-nil when the +reference has associated notes." + ;; REVIEW change to key only arg? :group 'citar :type '(function)) @@ -257,6 +259,7 @@ A note function must take two arguments: KEY: a string to represent the citekey ENTRY: an alist with the structured data (title, author, etc.)" + ;; REVIEW change to key only arg? :group 'citar :type 'function) @@ -269,6 +272,7 @@ A note function must take three arguments: KEY: a string to represent the citekey ENTRY: an alist with the structured data (title, author, etc.) FILEPATH: the file name." + ;; REVIEW change to key only arg? :group 'citar :type 'function) @@ -349,6 +353,12 @@ of all citations in the current buffer." :group 'citar :type '(repeat string)) +(defcustom citar-select-multiple t + "Use `completing-read-multiple' for selecting citation keys. +When nil, all citar commands will use `completing-read`." + :type 'boolean + :group 'citar) + ;;; Keymaps (defvar citar-map @@ -380,24 +390,34 @@ of all citations in the current buffer." map) "Keymap for Embark citation-key actions.") -;;; Completion functions +;; Internal variables -(defcustom citar-select-multiple t - "Use `completing-read-multiple' for selecting citation keys. -When nil, all citar commands will use `completing-read`." - :type 'boolean - :group 'citar) +;; Most of this design is adapted from org-mode 'oc-basic', +;; written by Nicolas Goaziou. + +(defvar citar--bibliography-cache nil + "Cache for parsed bibliography files. +This is an association list following the pattern: + (FILE-ID . ENTRIES) +FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of +the bibliography file, and HASH a hash of its contents. +ENTRIES is a hash table with citation references as keys and fields alist as +values.") + +(defvar citar--completion-cache (make-hash-table :test #'equal) + "Hash with key as completion string, value as citekey.") + +;;; Completion functions (defun citar--completion-table (candidates &optional filter &rest metadata) "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). +CANDIDATES is a hash with references CAND as key and CITEKEY as value, + where CAND is a display string for the bibliography item. 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. + argument KEY. Only candidates for which this function returns + non-nil will be offered for completion. By default the metadata of the table contains the category and affixation function. METADATA are extra entries for metadata of @@ -406,20 +426,22 @@ the form (KEY . VAL). The returned completion table can be used with `completing-read` and other completion functions." (let ((metadata `(metadata . ((category . citar-reference) - . ((affixation-function . ,#'citar--affixation) + . ((affixation-function . ,#'citar--ref-affix) . ,metadata))))) (lambda (string predicate action) (if (eq action 'metadata) metadata + ;; REVIEW this now works, but probably needs refinement (let ((predicate (when (or filter predicate) - (lambda (cand-key-entry) - (pcase-let ((`(,cand ,key . ,entry) cand-key-entry)) + (lambda (cand _) + (let* ((key (gethash cand candidates)) + (entry (citar--get-entry key))) (and (or (null filter) (funcall filter key entry)) - (or (null predicate) (funcall predicate cand)))))))) + (or (null predicate) (funcall predicate string)))))))) (complete-with-action action candidates string predicate)))))) -(cl-defun citar-select-ref (&optional &key rebuild-cache multiple filter) +(cl-defun citar-select-ref (&optional &key multiple filter) "Select bibliographic references. A wrapper around `completing-read' that returns (KEY . ENTRY), @@ -429,9 +451,6 @@ data. Takes the following optional keyword arguments: -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. @@ -440,49 +459,32 @@ FILTER: if non-nil, should be a predicate function taking 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 'citar-has-library-file-p) - (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)) + (citar-select-ref :filter 'citar-has-note-p)" + ;; TODO readd an example filter or two above? + (let* ((candidates (or (citar--ref-completion-table) + (user-error "No bibliography set"))) (chosen (if (and multiple citar-select-multiple) (citar--select-multiple "References: " candidates filter 'citar-history citar-presets) (completing-read "Reference: " (citar--completion-table candidates filter) - nil nil nil 'citar-history citar-presets nil))) - (notfound nil) - (keyentries - (seq-mapcat - ;; Find citation key-entry of selected candidate. - ;; CHOICE is either the formatted candidate string, or the citation - ;; key when called through `embark-act`. To handle both cases, test - ;; CHOICE against the first two elements of the entries of - ;; CANDIDATES. See - ;; https://github.com/bdarcus/citar/issues/233#issuecomment-901536901 - (lambda (choice) - (if-let ((cand (seq-find - (lambda (cand) (member choice (seq-take cand 2))) - candidates))) - (list (cdr cand)) - ;; If not found, add CHOICE to NOTFOUND and return nil - (push choice notfound) - nil)) - (if (listp chosen) chosen (list chosen))))) - (when notfound - (message "Keys not found: %s" (mapconcat #'identity notfound "; "))) - (if multiple keyentries (car keyentries)))) - -(cl-defun citar-select-refs (&optional &key rebuild-cache filter) + nil nil nil 'citar-history citar-presets nil)))) + ;; Return a list of keys regardless of 1 or many + (if (stringp chosen) + (list (gethash chosen candidates)) + (seq-map + (lambda (choice) + (gethash choice candidates)) + chosen)))) + +(cl-defun citar-select-refs (&optional &key filter) "Select bibliographic references. 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)) + (citar-select-ref :multiple t :filter filter)) (defun citar--multiple-completion-table (selected-hash candidates filter) "Return a completion table for multiple selection. @@ -530,7 +532,7 @@ HISTORY is the `completing-read' history argument." (completing-read (format "%s (%s/%s): " prompt (hash-table-count selected-hash) - (length candidates)) + (hash-table-count candidates)) (citar--multiple-completion-table selected-hash candidates filter) nil t nil history `("" . ,def))))) (unless (equal item "") @@ -576,6 +578,88 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) +(defun citar--ref-completion-table () + "Return completion table for cite keys, as a hash table. +In this hash table, keys are a strings with author, date, and +title of the reference. Values are the cite keys. +Return nil if there are no bibliography files or no entries." + ;; Populate bibliography cache. + (let* ((entries (citar--parse-bibliography)) + (hasnotep (citar-has-note)) + (hasfilep (citar-has-file)) + (mainwidth (citar--format-width (citar--get-template 'main))) + (suffixwidth (citar--format-width (citar--get-template 'suffix))) + (symbolswidth (string-width (citar--symbols-string t t t))) + (starwidth + (- (frame-width) (+ 2 symbolswidth mainwidth suffixwidth)))) + (cond + ((null entries) nil) ; no bibliography files + ;; if completion-cache is same as bibliography-cache, use the former + ((gethash entries citar--completion-cache) + citar--completion-cache) ; REVIEW ? + (t + (clrhash citar--completion-cache) + (dolist (key (citar--all-keys)) + (let* ((entry (citar--get-entry key)) + (hasfile + (when (funcall hasfilep key entry) "has:file")) + (hasnote + (when (funcall hasnotep key entry) "has:note")) + (candidatemain + (citar--format-entry + entry + starwidth + (citar--get-template 'main))) + (candidatesuffix + (citar--format-entry + entry + starwidth + (citar--get-template 'suffix))) + (invisible (concat hasfile " " hasnote)) + (completion + (string-trim-right + (concat + (propertize candidatemain 'face 'citar-highlight) " " + (propertize candidatesuffix 'face 'citar) + (propertize invisible 'invisible t))))) + (puthash completion key citar--completion-cache))) + (unless (map-empty-p citar--completion-cache) ; no key + (puthash entries t citar--completion-cache) ; REVIEW ? + citar--completion-cache))))) + +;; adapted from 'org-cite-basic--parse-bibliography' +(defvar citar--file-id-cache nil + "Hash table linking files to their hash.") + +(defun citar--parse-bibliography () + "List all entries available in the buffer. +Each association follows the pattern + (FILE . ENTRIES) +where FILE is the absolute file name of the bibliography file, +and ENTRIES is a hash table where keys are references and values +are association lists between fields, as symbols, and values as +strings or nil." + (unless (hash-table-p citar--file-id-cache) + (setq citar--file-id-cache (make-hash-table :test #'equal))) + (let ((results nil)) + ;; FIX the files to parse needs to be a function that returns the right + ;; local and/or global bibliography files for the current buffer. + (dolist (file citar-bibliography) + (when (file-readable-p file) + (with-temp-buffer + (when (or (file-has-changed-p file) + (not (gethash file citar--file-id-cache))) + (insert-file-contents file) + (puthash file (md5 (current-buffer)) citar--file-id-cache)) + (let* ((file-id (cons file (gethash file citar--file-id-cache))) + (entries + (or (cdr (assoc file-id citar--bibliography-cache)) + (let ((table (parsebib-parse file))) + (push (cons file-id table) citar--bibliography-cache) + table)))) + (push (cons file entries) results))))) + results)) + (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. If no function is found matching KEY for the current major mode, @@ -603,9 +687,36 @@ If no function is found, the DEFAULT function is called." (citar-file--normalize-paths citar-bibliography))) -(defun citar--get-value (field entry) - "Return the FIELD value for ENTRY." - (cdr (assoc-string field entry 'case-fold))) +;; Data access functions + +(cl-defun citar-get-data-entries (&optional &key filter) + "Return a subset of entries in bibliography by FILTER. + + (citar-get-data-entries :filter (citar-has-note))" + (let ((results (make-hash-table :test #'equal))) + (dolist (bibliography citar--bibliography-cache) + (maphash + (lambda (citekey entry) + (when (funcall filter citekey) + (puthash citekey entry results))) + (cdr bibliography))) + results)) + +(defun citar--get-entry (key) + "Return entry for KEY, as an association list." + (catch :found + ;; Iterate through the cached bibliography hashes and find a key. + (pcase-dolist (`(,_ . ,entries) (citar--parse-bibliography)) + (let ((entry (gethash key entries))) + (when entry (throw :found entry)))) + nil)) + +(defun citar--get-value (field key-or-entry) + "Return FIELD value for KEY-OR-ENTRY." + (let ((entry (if (stringp key-or-entry) + (citar--get-entry key-or-entry) + key-or-entry))) + (cdr (assoc-string field entry)))) (defun citar--field-with-value (fields entry) "Return the first field that has a value in ENTRY among FIELDS ." @@ -694,74 +805,32 @@ repeatedly." ;; Call each function in `citar-has-note-functions` to get a list of predicates (let ((preds (mapcar #'funcall citar-has-note-functions))) ;; Return a predicate that checks if `citekey` and `entry` have a note - (lambda (citekey entry) - ;; Call each predicate with `citekey` and `entry`; return the first non-nil result - (seq-some (lambda (pred) (funcall pred citekey entry)) preds)))) - -(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 -key associated with each one." - (let* ((candidates nil) - (raw-candidates - (parsebib-parse bib-files :fields (citar--fields-to-parse))) - (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))) - (star-width (- (frame-width) (+ 2 symbols-width main-width suffix-width)))) - (maphash - (lambda (citekey entry) - (let* ((files (when (funcall hasfilep citekey entry) " has:files")) - (notes (when (funcall hasnotep citekey entry) " has:notes")) - (link (when (citar--field-with-value '("doi" "url") entry) "has:link")) - (candidate-main - (citar--format-entry - entry - star-width - (citar--get-template 'main))) - (candidate-suffix - (citar--format-entry - entry - star-width - (citar--get-template 'suffix))) - ;; We display this content already using symbols; here we add back - ;; text to allow it to be searched, and citekey to ensure uniqueness - ;; of the candidate. - (candidate-hidden (string-join (list files notes link context citekey) " "))) - (when files (push (cons "has-file" t) entry)) - (when notes (push (cons "has-note" t) entry)) - (push - (cons - ;; If we don't trim the trailing whitespace, - ;; 'completing-read-multiple' will get confused when there are - ;; multiple selected candidates. - (string-trim-right - (concat - ;; We need all of these searchable: - ;; 1. the 'candidate-main' variable to be displayed - ;; 2. the 'candidate-suffix' variable to be displayed with a different face - ;; 3. the 'candidate-hidden' variable to be hidden - (propertize candidate-main 'face 'citar-highlight) " " - (propertize candidate-suffix 'face 'citar) " " - (propertize candidate-hidden 'invisible t))) - (cons citekey entry)) - candidates))) - raw-candidates) - candidates)) - -(defun citar--affixation (cands) + (lambda (citekey &optional entry) + (let ((nentry (or entry (citar--get-entry citekey)))) + ;; Call each predicate with `citekey` and `entry`; return the first non-nil result + (seq-some (lambda (pred) (funcall pred citekey nentry)) preds))))) + +(defun citar--ref-affix (cands) "Add affixation prefix to CANDS." (seq-map (lambda (candidate) - (let ((candidate-symbols (citar--symbols-string - (string-match "has:files" candidate) - (string-match "has:notes" candidate) - (string-match "has:link" candidate)))) - (list candidate candidate-symbols ""))) + (let ((symbols (citar--ref-make-symbols candidate))) + (list candidate symbols ""))) cands)) +(defun citar--ref-make-symbols (cand) + "Make CAND annotation or affixation string for has-symbols." + (let ((candidate-symbols (citar--symbols-string + (string-match "has:file" cand) + (string-match "has:note" cand) + (string-match "has:link" cand)))) + candidate-symbols)) + +(defun citar--ref-annotate (cand) + "Add annotation to CAND." + ;; REVIEW/TODO we don't currently use this, but could, for Emacs 27. + (citar--ref-make-symbols cand)) + (defun citar--symbols-string (has-files has-note has-link) "String for display from booleans HAS-FILES HAS-LINK HAS-NOTE." (cl-flet ((thing-string (has-thing thing-symbol) @@ -782,38 +851,6 @@ key associated with each one." "") ""))) -(defvar citar--candidates-cache 'uninitialized - "Store the global candidates list. - -Default value of `uninitialized' is used to indicate that cache -has not yet been created.") - -(defvar-local citar--local-candidates-cache 'uninitialized - ;; We use defvar-local so can maintain per-buffer candidate caches. - "Store the local (per-buffer) candidates list.") - -;;;###autoload -(defun citar-refresh (&optional force-rebuild-cache scope) - "Reload the candidates cache. - -If called interactively with a prefix or if FORCE-REBUILD-CACHE -is non-nil, also run the `citar-before-refresh-hook' hook. - -If SCOPE is `global' only global cache is refreshed, if it is -`local' only local cache is refreshed. With any other value both -are refreshed." - (interactive (list current-prefix-arg nil)) - (when force-rebuild-cache - (run-hooks 'citar-force-refresh-hook)) - (unless (eq 'local scope) - (setq citar--candidates-cache - (citar--format-candidates - (citar-file--normalize-paths citar-bibliography)))) - (unless (eq 'global scope) - (setq citar--local-candidates-cache - (citar--format-candidates - (citar--local-files-to-cache) "is:local")))) - (defun citar--get-template (template-name) "Return template string for TEMPLATE-NAME." (let ((template @@ -822,40 +859,11 @@ are refreshed." (error "No template for \"%s\" - check variable 'citar-templates'" template-name)) template)) -(defun citar--get-candidates (&optional force-rebuild-cache filter) - "Get the cached candidates. - -If the cache is unintialized, this will load the cache. - -If FORCE-REBUILD-CACHE is t, force reload the cache. - -If FILTER, use the function to filter the candidate list." - (when force-rebuild-cache - (citar-refresh force-rebuild-cache)) - (when (eq 'uninitialized citar--candidates-cache) - (citar-refresh nil 'global)) - (when (eq 'uninitialized citar--local-candidates-cache) - (citar-refresh nil 'local)) - (let ((candidates - (seq-concatenate 'list - citar--local-candidates-cache - citar--candidates-cache))) - (if candidates - (if filter - (seq-filter - (pcase-lambda (`(_ ,citekey . ,entry)) - (funcall filter citekey entry)) - candidates) - candidates) - (unless (or citar--candidates-cache citar--local-candidates-cache) - (error "Make sure to set citar-bibliography and related paths")) ))) - -(defun citar--get-entry (key) - "Return the cached entry for KEY." - (cddr (seq-find - (lambda (entry) - (string-equal key (cadr entry))) - (citar--get-candidates)))) +(defun citar--all-keys () + "List all keys available in current bibliography." + (seq-mapcat (pcase-lambda (`(,_ . ,entries)) + (map-keys entries)) + (citar--parse-bibliography))) (defun citar--get-link (entry) "Return a link for an ENTRY." @@ -867,62 +875,7 @@ If FILTER, use the function to filter the candidate list." (when field (concat base-url (citar--get-value field entry))))) -(defun citar--extract-keys (keys-entries) - "Extract list of keys from KEYS-ENTRIES. - -Each element of KEYS-ENTRIES should be either a (KEY . ENTRY) -pair or a string KEYS. - -- If it is a (KEY . ENTRY) pair, it is replaced by KEY in the - returned list. - -- Otherwise, it should be a string KEYS consisting of multiple - keys separated by \" & \". The string is split and the - separated keys are included in the returned list. - -Return a list containing only KEY strings." - (seq-mapcat - (lambda (key-entry) - (if (consp key-entry) - (list (car key-entry)) - (split-string key-entry " & "))) - keys-entries)) - -(defun citar--ensure-entries (keys-entries) - "Return copy of KEYS-ENTRIES with every element a (KEY . ENTRY) pair. - -Each element of KEYS-ENTRIES should be either a (KEY . ENTRY) -pair or a string KEYS. - -- If it is a (KEY . ENTRY) pair, it is included in the returned - list. - -- Otherwise, it should be a string KEYS consisting of multiple - keys separated by \" & \". Look up the corresponding ENTRY for - each KEY and, if found, include the (KEY . ENTRY) pairs in the - returned list. - -Return a list containing only (KEY . ENTRY) pairs." - (if (seq-every-p #'consp keys-entries) - keys-entries - ;; Get candidates only if some key has a missing entry, to avoid nasty - ;; recursion issues like https://github.com/bdarcus/citar/issues/286. Also - ;; avoids lots of memory allocation in the common case when all entries are - ;; present. - (let ((candidates (citar--get-candidates))) - (seq-mapcat - (lambda (key-entry) - (if (consp key-entry) - (list key-entry) - (seq-remove ; remove keys not found in CANDIDATES - #'null - (seq-map - (lambda (key) - (cdr (seq-find (lambda (cand-key-entry) - (string= key (cadr cand-key-entry))) - candidates))) - (split-string key-entry " & "))))) - keys-entries)))) +;; REVIEW I removed 'citar--ensure-entries' ;;;###autoload (defun citar-insert-preset () @@ -1021,6 +974,7 @@ FORMAT-STRING." "Look up key for a citar-reference TYPE and TARGET." (cons type (or (cadr (assoc target (with-current-buffer (embark--target-buffer) + ;; FIX how? (citar--get-candidates))))))) (defun citar--embark-selected () @@ -1062,11 +1016,10 @@ FORMAT-STRING." ;;; Commands ;;;###autoload -(defun citar-open (keys-entries) - "Open related resources (links or files) for KEYS-ENTRIES." +(defun citar-open (keys) + "Open related resources (links or files) for KEYS." (interactive (list - (list (citar-select-ref - :rebuild-cache current-prefix-arg)))) + (list (citar-select-ref)))) (when (and citar-library-paths (stringp citar-library-paths)) (message "Make sure 'citar-library-paths' is a list of paths")) @@ -1077,15 +1030,15 @@ FORMAT-STRING." (key-entry-alist (citar--ensure-entries keys-entries)) (files (citar-file--files-for-multiple-entries - key-entry-alist + keys (append citar-library-paths citar-notes-paths) ;; find files with any extension: nil)) (links (seq-map - (lambda (key-entry) - (citar--get-link (cdr key-entry))) - key-entry-alist)) + (lambda (key) + (citar--get-link key)) + keys)) (resource-candidates (delete-dups (append files (remq nil links))))) (cond ((eq nil resource-candidates) @@ -1107,14 +1060,12 @@ For use with `embark-act-all'." (find-file selection)) (t (citar-file-open selection)))) -(defun citar--library-file-action (key-entry action) - "Run ACTION on file associated with KEY-ENTRY." +(defun citar--library-file-action (key action) + "Run ACTION on file associated with KEY." (let* ((fn (pcase action ('open 'citar-file-open) ('attach 'mml-attach-file))) - (ke (citar--ensure-entries key-entry)) - (key (caar ke)) - (entry (cdar ke)) + (entry (citar--get-entry key)) (files (citar-file--files-for-entry key @@ -1135,25 +1086,23 @@ For use with `embark-act-all'." "Open library file associated with the KEY-ENTRY. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-ref))) (let ((embark-default-action-overrides '((file . citar-file-open)))) (when (and citar-library-paths (stringp citar-library-paths)) (error "Make sure 'citar-library-paths' is a list of paths")) - (citar--library-file-action key-entry 'open))) + (citar--library-file-action key 'open))) ;;;###autoload -(defun citar-open-notes (key-entry) - "Open notes associated with the KEY-ENTRY. +(defun citar-open-notes (key) + "Open notes associated with the KEY. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) + ;; REVIEW KEY, or KEYS + (interactive (list (citar-select-ref))) (let* ((embark-default-action-overrides '((file . find-file))) - (key (car key-entry)) - (entry (cdr key-entry))) + (entry (citar--get-entry key))) (if (listp citar-open-note-functions) - (citar--open-notes key entry) + (citar--open-notes (car key) entry) (error "Please change the value of 'citar-open-note-functions' to a list")))) (defun citar--open-notes (key entry) @@ -1164,26 +1113,23 @@ With prefix, rebuild the cache before offering candidates." (funcall citar-create-note-function key entry))) ;;;###autoload -(defun citar-open-entry (key-entry) - "Open bibliographic entry associated with the KEY-ENTRY. +(defun citar-open-entry (key) + "Open bibliographic entry associated with the KEY. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) - (when-let* ((key (car key-entry)) - (bibtex-files - (seq-concatenate - 'list - citar-bibliography - (citar--local-files-to-cache)))) - (bibtex-search-entry key t nil t))) + (interactive (list (citar-select-ref))) + (when-let ((bibtex-files + (seq-concatenate + 'list + citar-bibliography + (citar--local-files-to-cache)))) + (bibtex-search-entry (car key) t nil t))) ;;;###autoload -(defun citar-insert-bibtex (keys-entries) - "Insert bibliographic entry associated with the KEYS-ENTRIES. +(defun citar-insert-bibtex (keys) + "Insert bibliographic entry associated with the KEYS. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-refs - :rebuild-cache current-prefix-arg))) - (dolist (key (citar--extract-keys keys-entries)) + (interactive (list (citar-select-refs))) + (dolist (key keys) (citar--insert-bibtex key))) (defun citar--insert-bibtex (key) @@ -1218,20 +1164,21 @@ directory as current buffer." (citar--insert-bibtex key))))) ;;;###autoload -(defun citar-open-link (key-entry) - "Open URL or DOI link associated with the KEY-ENTRY in a browser. +(defun citar-open-link (key) + "Open URL or DOI link associated with the KEY in a browser. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref :rebuild-cache current-prefix-arg))) - (let ((link (citar--get-link (cdr key-entry)))) + (let* ((entry (citar--get-entry key)) + (link (citar--get-link entry))) (if link (browse-url link) - (message "No link found for %s" (car key-entry))))) + (message "No link found for %s" key)))) ;;;###autoload -(defun citar-insert-citation (keys-entries &optional arg) - "Insert citation for the KEYS-ENTRIES. +(defun citar-insert-citation (keys &optional arg) + "Insert citation for the KEYS. Prefix ARG is passed to the mode-specific insertion function. It should invert the default behaviour for that mode with respect to @@ -1246,7 +1193,7 @@ citation styles. See specific functions for more detail." (citar--major-mode-function 'insert-citation #'ignore - (citar--extract-keys keys-entries) + keys arg)) (defun citar-insert-edit (&optional arg) @@ -1261,57 +1208,54 @@ ARG is forwarded to the mode-specific insertion function given in arg)) ;;;###autoload -(defun citar-insert-reference (keys-entries) - "Insert formatted reference(s) associated with the KEYS-ENTRIES." +(defun citar-insert-reference (keys) + "Insert formatted reference(s) associated with the KEYS." (interactive (list (citar-select-refs))) - (let ((key-entry-alist (citar--ensure-entries keys-entries))) - (insert (funcall citar-format-reference-function key-entry-alist)))) + (insert (funcall citar-format-reference-function keys))) ;;;###autoload -(defun citar-copy-reference (keys-entries) - "Copy formatted reference(s) associated with the KEYS-ENTRIES." +(defun citar-copy-reference (keys) + "Copy formatted reference(s) associated with the KEYS." (interactive (list (citar-select-refs))) - (let* ((key-entry-alist (citar--ensure-entries keys-entries)) - (references (funcall citar-format-reference-function key-entry-alist))) + (let ((references (funcall citar-format-reference-function keys))) (if (not (equal "" references)) (progn (kill-new references) (message (format "Copied:\n%s" references))) (message "Key not found.")))) -(defun citar-format-reference (key-entry-alist) - "Return formatted reference(s) for the elements of KEY-ENTRY-ALIST." +(defun citar-format-reference (keys) + "Return formatted reference(s) for the elements of KEYS." (let* ((template (citar--get-template 'preview)) (references (with-temp-buffer - (dolist (key-entry key-entry-alist) + (dolist (key keys) (when template - (insert (citar--format-entry-no-widths (cdr key-entry) template)))) + (insert (citar--format-entry-no-widths key template)))) (buffer-string)))) references)) ;;;###autoload -(defun citar-insert-keys (keys-entries) - "Insert KEYS-ENTRIES citekeys. +(defun citar-insert-keys (keys) + "Insert KEYS citekeys. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-refs :rebuild-cache current-prefix-arg))) (citar--major-mode-function 'insert-keys #'citar--insert-keys-comma-separated - (citar--extract-keys keys-entries))) + keys)) (defun citar--insert-keys-comma-separated (keys) "Insert comma separated KEYS." (insert (string-join keys ", "))) ;;;###autoload -(defun citar-attach-library-file (key-entry) - "Attach library file associated with KEY-ENTRY to outgoing MIME message. +(defun citar-attach-library-file (key) + "Attach library file associated with KEY to outgoing MIME message. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-ref))) (let ((embark-default-action-overrides '((file . mml-attach-file)))) (when (and citar-library-paths (stringp citar-library-paths)) @@ -1350,8 +1294,8 @@ URL." (url-copy-file url (concat file-path extension) 1))))))) ;;;###autoload -(defun citar-add-file-to-library (key-entry) - "Add a file to the library for KEY-ENTRY. +(defun citar-add-file-to-library (key) + "Add a file to the library for KEY. The FILE can be added either from an open buffer, a file, or a URL." (interactive (list (citar-select-ref @@ -1359,9 +1303,9 @@ URL." (citar--add-file-to-library (car key-entry))) ;;;###autoload -(defun citar-run-default-action (keys-entries) - "Run the default action `citar-default-action' on KEYS-ENTRIES." - (funcall citar-default-action keys-entries)) +(defun citar-run-default-action (keys) + "Run the default action `citar-default-action' on KEYS." + (funcall citar-default-action keys)) ;;;###autoload (defun citar-dwim () From a10bfa374beacbe2a47247cbb277260d6325ee04 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 10 Jun 2022 09:49:38 -0400 Subject: [PATCH 18/78] Add citar--bibliography-files This functions returns all local and global bibliography files for 'citar--parse-bibliography' to parse. --- citar.el | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/citar.el b/citar.el index 572c4bb8..699743f3 100644 --- a/citar.el +++ b/citar.el @@ -459,10 +459,9 @@ FILTER: if non-nil, should be a predicate function taking function returns non-nil will be offered for completion. For example: - (citar-select-ref :filter 'citar-has-library-file-p) + (citar-select-ref :filter (citar-has-note)) - (citar-select-ref :filter 'citar-has-note-p)" - ;; TODO readd an example filter or two above? + (citar-select-ref :filter (citar-has-file))" (let* ((candidates (or (citar--ref-completion-table) (user-error "No bibliography set"))) (chosen (if (and multiple citar-select-multiple) @@ -578,6 +577,10 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) +(defun citar--bibliography-files () + "The list of global and local bibliography files." + (seq-concatenate 'list citar-bibliography (citar--local-files-to-cache))) + (defun citar--ref-completion-table () "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and @@ -642,9 +645,7 @@ strings or nil." (unless (hash-table-p citar--file-id-cache) (setq citar--file-id-cache (make-hash-table :test #'equal))) (let ((results nil)) - ;; FIX the files to parse needs to be a function that returns the right - ;; local and/or global bibliography files for the current buffer. - (dolist (file citar-bibliography) + (dolist (file (citar--bibliography-files)) (when (file-readable-p file) (with-temp-buffer (when (or (file-has-changed-p file) From 9e432fdaada28acc47ec85750cd6a02f6425f75b Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 10 Jun 2022 11:00:29 -0400 Subject: [PATCH 19/78] Add citar-prefilter-entries Allows to independently turn off whether to do this by default, and whether to toggle the behavior. --- citar.el | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/citar.el b/citar.el index 699743f3..5d8fc29a 100644 --- a/citar.el +++ b/citar.el @@ -180,6 +180,25 @@ All functions that match a particular field are run in order." :type '(alist :key-type (choice (const t) (repeat string)) :value-type function)) +(defcustom citar-prefilter-entries '(nil . t) + "When non-nil pre-filter note and library files commands. +For commands like 'citar-open-notes', this will only show +completion candidates that have such notes. + +The downside is that, if using Embark and you want to use a different +command for the action, you will not be able to remove the +filter. + +The value should be a cons of the form: + +(FILTER . TOGGLE) + +FILTER turns this on by default + +TOGGLE use prefix arg to toggle behavior" + :group 'citar + :type 'cons) + (defcustom citar-symbols `((file . ("F" . " ")) (note . ("N" . " ")) From 6b6c9aec75f9f4208ceeb47e79c266214ebe3159 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 10 Jun 2022 13:42:11 -0600 Subject: [PATCH 20/78] Add implementation of cache and new formatting functions. --- citar.el | 321 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 266 insertions(+), 55 deletions(-) diff --git a/citar.el b/citar.el index 5d8fc29a..e5f8542a 100644 --- a/citar.el +++ b/citar.el @@ -409,12 +409,41 @@ When nil, all citar commands will use `completing-read`." map) "Keymap for Embark citation-key actions.") -;; Internal variables - -;; Most of this design is adapted from org-mode 'oc-basic', -;; written by Nicolas Goaziou. - -(defvar citar--bibliography-cache nil +;;; Bibliography cache + +(cl-defstruct (citar--bibliography + (:constructor citar--make-bibliography (filename)) + (:copier nil)) + "Cached bibliography file." + (filename + nil + :read-only t + :documentation + "True filename of a bibliography, as returned by `file-truename`.") + (hash + nil + :documentation + "Hash of the file's contents, as returned by `buffer-hash`.") + (buffers + nil + :documentation + "List of buffers that require this bibliography.") + (entries + (make-hash-table :test 'equal) + :documentation + "Hash table mapping citation keys to bibliography entries, + as returned by `parsebib-parse`.") + (preformatted + (make-hash-table :test 'equal) + :documentation + "Pre-formatted strings used to display bibliography entries; + see `citar--preformatter`.") + (format-string + nil + :documentation + "Format string used to generate pre-formatted strings.")) + +(defvar citar--bibliography-cache (make-hash-table :test 'equal) "Cache for parsed bibliography files. This is an association list following the pattern: (FILE-ID . ENTRIES) @@ -423,8 +452,114 @@ the bibliography file, and HASH a hash of its contents. ENTRIES is a hash table with citation references as keys and fields alist as values.") -(defvar citar--completion-cache (make-hash-table :test #'equal) - "Hash with key as completion string, value as citekey.") +(defun citar--get-bibliography (filename &optional buffer) + "Return cached bibliography for FILENAME and associate it with BUFFER. +If FILENAME is not already cached, read and cache it. If BUFFER +is nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires the bibliography FILENAME, or +a symbol like 'global." + (let* ((buffer (cond ((null buffer) (current-buffer)) + ((symbolp buffer) buffer) + (t (get-buffer buffer)))) + (cached (gethash filename citar--bibliography-cache)) + (bib (or cached (citar--make-bibliography filename)))) + (unless cached + (citar--update-bibliography bib) + (puthash filename bib citar--bibliography-cache)) + (cl-pushnew buffer (citar--bibliography-buffers bib)) + (unless (symbolp buffer) + (with-current-buffer buffer + (dolist (hook '(change-major-mode-hook kill-buffer-hook)) + (add-hook hook #'citar--release-bibliographies 0 'local)))) + bib)) + +(defun citar--cache-bibliographies (filenames &optional buffer) + "Return cached bibliographies for FILENAMES and associate them with BUFFER. +FILENAMES is a list of bibliography file names. If BUFFER is +nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires these bibliographies, or a +symbol like 'global. + +Remove any existing associations between BUFFER and cached files +not included in FILENAMES. Release cached files that are no +longer needed by any other buffer. + +Return a list of `citar--bibliography` objects, one for each +element of FILENAMES." + (citar--release-bibliographies filenames buffer) + (mapcar + (lambda (filename) (citar--get-bibliography filename buffer)) + filenames)) + +(defun citar--release-bibliographies (&optional keep-filenames buffer) + "Dissociate BUFFER from cached bibliographies. +If BUFFER is nil, use the current buffer. Otherwise, BUFFER +should be a buffer object, buffer name, or a symbol like 'global. +KEEP-FILENAMES is a list of file names that are not dissociated +from BUFFER. + +Remove any bibliographies from the cache that are no longer +needed by any other buffer." + (let ((buffer (cond ((null buffer) (current-buffer)) + ((symbolp buffer) buffer) + (t (get-buffer buffer))))) + (maphash + (lambda (filename bib) + (unless (member filename keep-filenames) + (setf (citar--bibliography-buffers bib) + (delq buffer (citar--bibliography-buffers bib))) + (unless (citar--bibliography-buffers bib) + (citar--delete-bibliography-from-cache filename)))) + citar--bibliography-cache))) + +(defun citar--bibliographies (&rest buffers) + "Return bibliographies for BUFFERS." + (delete-dups + (mapcan + (lambda (buffer) + (citar--cache-bibliographies (citar--bibliography-files buffer) buffer)) + (or buffers (list (current-buffer) 'global))))) + +(defun citar--delete-bibliography-from-cache (filename) + "Remove bibliography cache entry for FILENAME." + ;; TODO Perform other needed actions, like removing filenotify watches + (remhash filename citar--bibliography-cache)) + +(defun citar--update-bibliography (bib &optional force) + "Update the bibliography BIB from the original file. + +Unless FORCE is non-nil, the file is re-read only if it has been +modified since the last time BIB was updated." + (let* ((filename (citar--bibliography-filename bib)) + (entries (citar--bibliography-entries bib)) + (preformatted (citar--bibliography-preformatted bib)) + (formatstring (citar--bibliography-format-string bib)) + (newformatstring + (concat (propertize (citar--get-template 'main) 'face 'citar-highlight) + (propertize (citar--get-template 'suffix) 'face 'citar))) + (newhash + (with-temp-buffer + (insert-file-contents filename) + (buffer-hash)))) + ;; TODO Also check file size and modification time before hashing? + ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` + (unless (or force + (and (equal newhash (citar--bibliography-hash bib)) + (equal-including-properties formatstring newformatstring))) + ;; Update entries + (clrhash entries) + (parsebib-parse filename + :entries entries + :fields (citar--fields-to-parse)) + (setf (citar--bibliography-hash bib) newhash) + ;; Update preformatted strings + (clrhash preformatted) + (let ((preformatter (citar--preformatter newformatstring))) + (maphash + (lambda (citekey entry) + (puthash citekey (funcall preformatter entry) preformatted)) + entries)) + (setf (citar--bibliography-format-string bib) newformatstring)))) ;;; Completion functions @@ -596,9 +731,22 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) -(defun citar--bibliography-files () - "The list of global and local bibliography files." - (seq-concatenate 'list citar-bibliography (citar--local-files-to-cache))) +(defun citar--bibliography-files (&rest buffers) + "Bibliography file names for BUFFERS. +The elements of BUFFERS are either buffers or the symbol 'global. +Returns the absolute file names of the bibliographies in all +these contexts. + +When BUFFERS is empty, return local bibliographies for the +current buffer and global bibliographies." + (citar-file--normalize-paths + (mapcan (lambda (buffer) + (if (eq buffer 'global) + (if (listp citar-bibliography) citar-bibliography + (list citar-bibliography)) + (with-current-buffer buffer + (citar--major-mode-function 'local-bib-files #'ignore)))) + (or buffers (list (current-buffer) 'global))))) (defun citar--ref-completion-table () "Return completion table for cite keys, as a hash table. @@ -615,10 +763,10 @@ Return nil if there are no bibliography files or no entries." (starwidth (- (frame-width) (+ 2 symbolswidth mainwidth suffixwidth)))) (cond - ((null entries) nil) ; no bibliography files + ((null entries) nil) ; no bibliography files ;; if completion-cache is same as bibliography-cache, use the former ((gethash entries citar--completion-cache) - citar--completion-cache) ; REVIEW ? + citar--completion-cache) ; REVIEW ? (t (clrhash citar--completion-cache) (dolist (key (citar--all-keys)) @@ -649,37 +797,6 @@ Return nil if there are no bibliography files or no entries." (puthash entries t citar--completion-cache) ; REVIEW ? citar--completion-cache))))) -;; adapted from 'org-cite-basic--parse-bibliography' -(defvar citar--file-id-cache nil - "Hash table linking files to their hash.") - -(defun citar--parse-bibliography () - "List all entries available in the buffer. -Each association follows the pattern - (FILE . ENTRIES) -where FILE is the absolute file name of the bibliography file, -and ENTRIES is a hash table where keys are references and values -are association lists between fields, as symbols, and values as -strings or nil." - (unless (hash-table-p citar--file-id-cache) - (setq citar--file-id-cache (make-hash-table :test #'equal))) - (let ((results nil)) - (dolist (file (citar--bibliography-files)) - (when (file-readable-p file) - (with-temp-buffer - (when (or (file-has-changed-p file) - (not (gethash file citar--file-id-cache))) - (insert-file-contents file) - (puthash file (md5 (current-buffer)) citar--file-id-cache)) - (let* ((file-id (cons file (gethash file citar--file-id-cache))) - (entries - (or (cdr (assoc file-id citar--bibliography-cache)) - (let ((table (parsebib-parse file))) - (push (cons file-id table) citar--bibliography-cache) - table)))) - (push (cons file entries) results))))) - results)) - (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. If no function is found matching KEY for the current major mode, @@ -699,14 +816,6 @@ return DEFAULT." If no function is found, the DEFAULT function is called." (apply (citar--get-major-mode-function key default) args)) -(defun citar--local-files-to-cache () - "The local bibliographic files not included in the global bibliography." - ;; We cache these locally to the buffer. - (seq-difference (citar-file--normalize-paths - (citar--major-mode-function 'local-bib-files #'ignore)) - (citar-file--normalize-paths - citar-bibliography))) - ;; Data access functions (cl-defun citar-get-data-entries (&optional &key filter) @@ -722,15 +831,24 @@ If no function is found, the DEFAULT function is called." (cdr bibliography))) results)) -(defun citar--get-entry (key) +(defun citar--get-entry (key &optional bibs) "Return entry for KEY, as an association list." (catch :found ;; Iterate through the cached bibliography hashes and find a key. - (pcase-dolist (`(,_ . ,entries) (citar--parse-bibliography)) - (let ((entry (gethash key entries))) + (dolist (bib (or bibs (citar--bibliographies))) + (let* ((entries (citar--bibliography-entries bib)) + (entry (gethash key entries))) (when entry (throw :found entry)))) nil)) +(defun citar--get-entries (&optional bibs) + (let ((entries (make-hash-table :test 'equal))) + (dolist (bib (reverse (or bibs (citar--bibliographies)))) + (maphash (lambda (citekey entry) + (puthash citekey entry entries)) + (citar--bibliography-entries bib))) + entries)) + (defun citar--get-value (field key-or-entry) "Return FIELD value for KEY-OR-ENTRY." (let ((entry (if (stringp key-or-entry) @@ -909,6 +1027,99 @@ repeatedly." ;;; Formatting functions +(defun citar--format-parse (format-string) + "Parse FORMAT-STRING." + (let ((regex (concat "\\${" ; ${ + "\\(.*?\\)" ; field names + "\\(?::[[:blank:]]*" ; : + space + "\\(.*?\\)" ; format spec + "[[:blank:]]*\\)?}")) ; space + } + (position 0) + (parsed nil)) + (while (string-match regex format-string position) + (let* ((begin (match-beginning 0)) + (end (match-end 0)) + (textprops (text-properties-at begin format-string)) + (fields (match-string-no-properties 1 format-string)) + (spec (match-string-no-properties 2 format-string)) + (width (cond + ((null spec) nil) + ((equal spec "*") '*) + (t (string-to-number spec))))) + (when (< position begin) + (push (substring format-string position begin) parsed)) + (setq position end) + (push (cons (nconc (when width `(:width ,width)) + (when textprops `(:text-properties ,textprops))) + (split-string-and-unquote fields)) + parsed))) + (when (< position (length format-string)) + (push (substring format-string position) parsed)) + (nreverse parsed))) + +(defun citar--preformat-parse (format-string) + "Parse and group FORMAT-STRING." + (let ((parsed (citar--format-parse format-string)) + groups group props) + (dolist (field parsed) + (unless props (setq props (list :width 0))) + (let ((fieldwidth (cond + ((stringp field) (string-width field)) + ((listp field) (plist-get (car field) :width))))) + (cond + ((eq fieldwidth '*) + (push (cons props (nreverse group)) groups) + (setcar field (plist-put (car field) :width nil)) + (push (cons '(:width *) (list field)) groups) + (setq group nil props nil)) + ((numberp fieldwidth) + (push field group) + (if-let* ((oldwidth (plist-get props :width)) + ((numberp oldwidth))) + (setq props (plist-put props :width (+ oldwidth fieldwidth))))) + (t + (push field group) + (setq props (plist-put props :width nil)))))) + (when group + (push (cons props (nreverse group)) groups)) + (nreverse groups))) + +(defun citar--preformatter (format-string) + "Preformat according to FORMAT-STRING." + (let ((groups (citar--preformat-parse format-string))) + (lambda (entry) + (mapcar + (pcase-lambda (`(,props . ,group)) + (citar--format-string-with-props + props + (mapconcat + (lambda (field) + (if (stringp field) + field + (apply #'citar--format-entry-with-props entry field))) + group ""))) + groups)))) + +(defun citar--format-preformatted (format-string) + (let* ((groups (citar--preformat-parse format-string)) + (widths (mapcar (lambda (group) (plist-get (car group) :width)) + groups)) + (nstars (cl-count '* ))) + (lambda (preformatted)))) + +(defun citar--format-string-with-props (props string) + (let ((width (plist-get props :width)) + (textprops (plist-get props :text-properties))) + (when textprops + (setq string (apply #'propertize string textprops))) + (when (numberp width) + (setq string (truncate-string-to-width string width 0 ?\s ))) + string)) + +(defun citar--format-entry-with-props (entry props &rest fields) + "Format FIELDS of ENTRY using PROPS." + (citar--format-string-with-props props (citar--display-value fields entry))) + (defun citar--format-width (format-string) "Calculate minimal width needed by the FORMAT-STRING." (let ((content-width (apply #'+ From ca1b8bfc7606928a606492d1604e815c5a99a2a4 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 13 Jun 2022 01:23:50 -0600 Subject: [PATCH 21/78] Move cache and formatting functions into new files --- citar-cache.el | 239 +++++++++++++++++++++++++ citar-file.el | 1 - citar-format.el | 225 +++++++++++++++++++++++ citar-org.el | 7 +- citar.el | 461 ++++++++---------------------------------------- 5 files changed, 538 insertions(+), 395 deletions(-) create mode 100644 citar-cache.el create mode 100644 citar-format.el diff --git a/citar-cache.el b/citar-cache.el new file mode 100644 index 00000000..784ba4b0 --- /dev/null +++ b/citar-cache.el @@ -0,0 +1,239 @@ +;;; citar-cache.el --- Cache functions for citar -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Bruce D'Arcus, Roshan Shariff +;; +;; This file is not part of GNU Emacs. +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . +;; +;;; Commentary: +;; +;; Functions for caching bibliography files. +;; +;;; Code: + +(eval-when-compile + (require 'cl-lib)) +(require 'parsebib) +(require 'citar-format) + +(declare-function citar--get-template "citar") +(declare-function citar--fields-to-parse "citar") + + +;;; Variables: + + +(defvar citar-cache--bibliographies (make-hash-table :test 'equal) + "Cache for parsed bibliography files. +This is an association list following the pattern: + (FILE-ID . ENTRIES) +FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of +the bibliography file, and HASH a hash of its contents. +ENTRIES is a hash table with citation references as keys and fields alist as +values.") + + +;;; Bibliography objects + + +(cl-defstruct (citar-cache--bibliography + (:constructor citar-cache--make-bibliography (filename)) + (:copier nil)) + "Cached bibliography file." + (filename + nil + :read-only t + :documentation + "True filename of a bibliography, as returned by `file-truename`.") + (hash + nil + :documentation + "Hash of the file's contents, as returned by `buffer-hash`.") + (buffers + nil + :documentation + "List of buffers that require this bibliography.") + (entries + (make-hash-table :test 'equal) + :documentation + "Hash table mapping citation keys to bibliography entries, + as returned by `parsebib-parse`.") + (preformatted + (make-hash-table :test 'equal) + :documentation + "Pre-formatted strings used to display bibliography entries; + see `citar--preformatter`.") + (format-string + nil + :documentation + "Format string used to generate pre-formatted strings.")) + + +(defun citar-cache--get-bibliographies (filenames &optional buffer) + "Return cached bibliographies for FILENAMES and associate them with BUFFER. +FILENAMES is a list of bibliography file names. If BUFFER is +nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires these bibliographies, or a +symbol like 'global. + +Remove any existing associations between BUFFER and cached files +not included in FILENAMES. Release cached files that are no +longer needed by any other buffer. + +Return a list of `citar--bibliography` objects, one for each +element of FILENAMES." + (citar-cache--release-bibliographies filenames buffer) + (mapcar + (lambda (filename) (citar-cache--get-bibliography filename buffer)) + filenames)) + +(defun citar-cache--entry (key bibs) + (catch :found + ;; Iterate through the cached bibliography hashes and find a key. + (dolist (bib bibs) + (let* ((entries (citar-cache--bibliography-entries bib)) + (entry (gethash key entries))) + (when entry (throw :found entry)))) + nil)) + +(defun citar-cache--entries (bibs) + (citar-cache--merge-hash-tables + (mapcar #'citar-cache--bibliography-entries bibs))) + +(defun citar-cache--preformatted (bibs) + (citar-cache--merge-hash-tables + (mapcar #'citar-cache--bibliography-preformatted bibs))) + + +;;; Creating and deleting bibliography caches + + +(defun citar-cache--get-bibliography (filename &optional buffer) + "Return cached bibliography for FILENAME and associate it with BUFFER. +If FILENAME is not already cached, read and cache it. If BUFFER +is nil, use the current buffer. Otherwise, BUFFER should be a +buffer object or name that requires the bibliography FILENAME, or +a symbol like 'global." + (let* ((cached (gethash filename citar-cache--bibliographies)) + (bib (or cached (citar-cache--make-bibliography filename))) + (buffer (citar-cache--canonicalize-buffer buffer)) + (fmtstr (citar--get-template 'completion))) + (unless cached + (setf (citar-cache--bibliography-format-string bib) fmtstr) + (citar-cache--update-bibliography bib) + (puthash filename bib citar-cache--bibliographies)) + ;; Preformat strings if format has changed + (unless (equal-including-properties + fmtstr (citar-cache--bibliography-format-string bib)) + (setf (citar-cache--bibliography-format-string bib) fmtstr) + (citar-cache--preformat-bibliography bib)) + ;; Associate buffer with this bibliography: + (cl-pushnew buffer (citar-cache--bibliography-buffers bib)) + ;; Release bibliography when buffer is killed or changes major mode: + (unless (symbolp buffer) + (with-current-buffer buffer + (dolist (hook '(change-major-mode-hook kill-buffer-hook)) + (add-hook hook #'citar-cache--release-bibliographies 0 'local)))) + bib)) + + +(defun citar-cache--release-bibliographies (&optional keep-filenames buffer) + "Dissociate BUFFER from cached bibliographies. +If BUFFER is nil, use the current buffer. Otherwise, BUFFER +should be a buffer object, buffer name, or a symbol like 'global. +KEEP-FILENAMES is a list of file names that are not dissociated +from BUFFER. + +Remove any bibliographies from the cache that are no longer +needed by any other buffer." + (let ((buffer (citar-cache--canonicalize-buffer buffer))) + (maphash + (lambda (filename bib) + (unless (member filename keep-filenames) + (cl-callf2 delq buffer (citar-cache--bibliography-buffers bib)) + (unless (citar-cache--bibliography-buffers bib) + (citar-cache--remove-bibliography filename)))) + citar-cache--bibliographies))) + + +(defun citar-cache--remove-bibliography (filename) + "Remove bibliography cache entry for FILENAME." + ;; TODO Perform other needed actions, like removing filenotify watches + (remhash filename citar-cache--bibliographies)) + + +;;; Updating bibliographies + + +(defun citar-cache--update-bibliography (bib &optional force) + "Update the bibliography BIB from the original file. + +Unless FORCE is non-nil, the file is re-read only if it has been +modified since the last time BIB was updated." + (let* ((filename (citar-cache--bibliography-filename bib)) + (entries (citar-cache--bibliography-entries bib)) + (newhash (with-temp-buffer + (insert-file-contents filename) + (buffer-hash)))) + ;; TODO Also check file size and modification time before hashing? + ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` + (unless (or force (equal newhash (citar-cache--bibliography-hash bib))) + ;; Update entries + (clrhash entries) + (parsebib-parse filename :entries entries :fields (citar--fields-to-parse)) + (setf (citar-cache--bibliography-hash bib) newhash) + ;; Update preformatted strings + (citar-cache--preformat-bibliography bib)))) + + +(defun citar-cache--preformat-bibliography (bib) + "Updated pre-formatted strings in BIB." + (let* ((entries (citar-cache--bibliography-entries bib)) + (fmtstr (citar-cache--bibliography-format-string bib)) + (preformat (citar-format--preformat fmtstr :hide-elided t)) + (preformatted (citar-cache--bibliography-preformatted bib))) + (clrhash preformatted) + (maphash (lambda (citekey entry) + (puthash citekey (funcall preformat entry) preformatted)) + entries))) + + +;;; Utility functions: + + +(defun citar-cache--canonicalize-buffer (buffer) + "Return buffer object or symbol denoted by BUFFER. +If BUFFER is nil, return the current buffer. Otherwise, BUFFER +should be a buffer object or name, or a symbol like 'global. If +it is a buffer object or symbol, it is returned as-is. +Otherwise, return the buffer object whose name is BUFFER." + (cond ((null buffer) (current-buffer)) + ((symbolp buffer) buffer) + (t (get-buffer buffer)))) + + +(defun citar-cache--merge-hash-tables (hash-tables) + "Merge hash tables in HASH-TABLES." + (when-let ((hash-tables (reverse hash-tables)) + (first (pop hash-tables))) + (if (null hash-tables) + first + (let ((result (copy-hash-table first))) + (dolist (table hash-tables result) + (maphash (lambda (key entry) (puthash key entry result)) table)))))) + + +(provide 'citar-cache) +;;; citar-cache.el ends here diff --git a/citar-file.el b/citar-file.el index 75e0dc3f..30d2d717 100644 --- a/citar-file.el +++ b/citar-file.el @@ -41,7 +41,6 @@ (declare-function citar--get-entry "citar") (declare-function citar--get-value "citar") (declare-function citar--get-template "citar") -(declare-function citar--format-entry-no-widths "citar") ;;;; File related variables diff --git a/citar-format.el b/citar-format.el new file mode 100644 index 00000000..73f8eabe --- /dev/null +++ b/citar-format.el @@ -0,0 +1,225 @@ +;;; citar-format.el --- Formatting functions for citar -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Bruce D'Arcus, Roshan Shariff +;; +;; This file is not part of GNU Emacs. +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . +;; +;;; Commentary: +;; +;; Functions for formatting bibliography entries. +;; +;;; Code: + +(eval-when-compile + (require 'cl-lib)) + +(declare-function citar--display-value "citar") +(defvar citar-ellipsis) + + +;;; Formatting bibliography entries + + +(cl-defun citar-format--entry (format-string entry &key width hide-elided + (ellipsis citar-ellipsis)) + "Format ENTRY according to FORMAT-STRING." + (cl-flet ((getwidth (fieldspec) + (unless (stringp fieldspec) + (plist-get (car fieldspec) :width))) + (fmtfield (fieldspec) + (citar-format--fieldspec fieldspec entry + :hide-elided hide-elided + :ellipsis ellipsis))) + (let* ((fieldspecs (citar-format--parse format-string)) + (widths (mapcar #'getwidth fieldspecs)) + (strings (mapcar #'fmtfield fieldspecs))) + (citar-format--star-widths widths strings :width width + :hide-elided hide-elided :ellipsis ellipsis)))) + + +;;; Pre-formatting bibliography entries + + +(cl-defun citar-format--preformat (format-string &key hide-elided + (ellipsis citar-ellipsis)) + "Preformat according to FORMAT-STRING. +See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." + (let ((fieldgroups (citar-format--preformat-parse format-string))) + (lambda (entry) + (cl-flet ((fmtfield (fieldspec) + (citar-format--fieldspec fieldspec entry + :hide-elided hide-elided + :ellipsis ellipsis))) + (mapcar (lambda (groupspec) + (mapconcat #'fmtfield (cdr groupspec) "")) + fieldgroups))))) + + +(cl-defun citar-format--preformatted (format-string &key width hide-elided + (ellipsis citar-ellipsis)) + "Fit pre-formatted strings to WIDTH according to FORMAT-STRING. +See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." + (let* ((fieldgroups (citar-format--preformat-parse format-string)) + (widths (mapcar (lambda (groupspec) + (plist-get (car groupspec) :width)) + fieldgroups))) + (lambda (preformatted) + (citar-format--star-widths widths preformatted :width width + :hide-elided hide-elided :ellipsis ellipsis)))) + + +;;; Internal implementation functions + + +(cl-defun citar-format--fieldspec (fieldspec entry &key hide-elided ellipsis) + "Format FIELDSPEC using information from ENTRY. +See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." + (if (stringp fieldspec) + fieldspec + (let* ((fmtprops (car fieldspec)) + (fieldnames (cdr fieldspec)) + (displaystr (citar--display-value fieldnames entry))) + (apply #'citar-format--string displaystr + :hide-elided hide-elided :ellipsis ellipsis + fmtprops)))) + + +(cl-defun citar-format--string (string + &key width text-properties hide-elided ellipsis) + "Truncate STRING to WIDTH and apply TEXT-PROPERTIES. +If HIDE-ELIDED is non-nil, the truncated part of STRING is +covered by a display property that makes it invisible, instead of +being deleted. ELLIPSIS, when non-nil, specifies a string to +display instead of the truncated part of the text." + (when text-properties + (setq string (apply #'propertize string text-properties))) + (when (numberp width) + (setq string (truncate-string-to-width string width 0 ?\s ellipsis hide-elided))) + string) + + +(cl-defun citar-format--star-widths (widths strings &key width + hide-elided ellipsis) + "Format STRINGS according to WIDTHS to fit WIDTH." + (if (not (and (numberp width) (cl-find '* widths))) + ;; If width is unlimited or there are no *-width fields, just join strings. + ;; We only support truncating *-width fields. + (string-join strings) + ;; Otherwise, calculate extra space available for *-width fields + (let ((usedwidth 0) (nstars 0)) + ;; For fields without width spec, add their actual width to usedwidth + (cl-mapc + (lambda (width string) + (cond ((eq '* width) (cl-incf nstars)) + ((numberp width) (cl-incf usedwidth width)) + ((null width) (cl-incf usedwidth (string-width string))))) + widths strings) + (let* ((extrawidth (max 0 (- width usedwidth))) + (starwidth (/ extrawidth nstars)) + (remainder (% extrawidth nstars)) + (starindex 0)) + (string-join + (cl-mapcar + (lambda (width string) + (if (not (eq width '*)) + string + (cl-incf starindex) + (citar-format--string + string + :width (+ starwidth (if (<= starindex remainder) 1 0)) + :hide-elided hide-elided :ellipsis ellipsis))) + widths strings)))))) + + +;;; Parsing format strings + + +(defun citar-format--parse (format-string) + "Parse FORMAT-STRING." + (let ((regex (concat "\\${" ; ${ + "\\(.*?\\)" ; field names + "\\(?::[[:blank:]]*" ; : + space + "\\(.*?\\)" ; format spec + "[[:blank:]]*\\)?}")) ; space + } + (position 0) + (fieldspecs nil)) + (while (string-match regex format-string position) + (let* ((begin (match-beginning 0)) + (end (match-end 0)) + (textprops (text-properties-at begin format-string)) + (fieldnames (match-string-no-properties 1 format-string)) + (spec (match-string-no-properties 2 format-string)) + (width (cond + ((or (null spec) (string-empty-p spec)) nil) + ((string-equal spec "*") '*) + (t (string-to-number spec))))) + (when (< position begin) + (push (substring format-string position begin) fieldspecs)) + (push (cons (nconc (when width `(:width ,width)) + (when textprops `(:text-properties ,textprops))) + (split-string-and-unquote fieldnames)) + fieldspecs) + (setq position end))) + (when (< position (length format-string)) + (push (substring format-string position) fieldspecs)) + (nreverse fieldspecs))) + + +(defun citar-format--preformat-parse (format-string) + "Parse and group FORMAT-STRING." + (let (fieldgroups group (groupwidth 0)) + (cl-flet ((newgroup () + (when group + (push (cons (when groupwidth `(:width ,groupwidth)) + (nreverse group)) + fieldgroups) + (setq group nil)) + (setq groupwidth 0))) + (dolist (fieldspec (citar-format--parse format-string)) + (let ((fieldwidth (cond + ((stringp fieldspec) (string-width fieldspec)) + ((listp fieldspec) (plist-get (car fieldspec) :width))))) + (cond + ((eq fieldwidth '*) + ;; *-width field; start a new group + (newgroup) + ;; Pre-format the field at unlimited width by setting :width to nil + (cl-callf plist-put (car fieldspec) :width nil) + ;; Add the field in its own pre-format group with :width * + (push fieldspec group) + (setq groupwidth '*) + (newgroup)) + ((numberp fieldwidth) + ;; Fixed-length field; start new group if needed + (unless (numberp groupwidth) + (newgroup)) + ;; Add field to group and increment group width + (push fieldspec group) + (cl-incf groupwidth fieldwidth)) + (t + ;; Unknown-length field; start new group if needed + (unless (null groupwidth) + (newgroup)) + ;; Add field to group; group width is now unknown + (push fieldspec group) + (setq groupwidth nil))))) + ;; Add any remaining fields to group + (newgroup)) + (nreverse fieldgroups))) + + +(provide 'citar-format) +;;; citar-format.el ends here diff --git a/citar-org.el b/citar-org.el index b04327d5..e76979ff 100644 --- a/citar-org.el +++ b/citar-org.el @@ -292,11 +292,8 @@ With optional argument FORCE, force the creation of a new ID." (defun citar-org-format-note-default (key entry filepath) "Format a note FILEPATH from KEY and ENTRY." (let* ((template (citar--get-template 'note)) - (note-meta - (when template - (citar--format-entry-no-widths - entry - template))) + (note-meta (when template + (citar-format--entry template entry))) (buffer (find-file filepath))) (with-current-buffer buffer ;; This just overrides other template insertion. diff --git a/citar.el b/citar.el index e5f8542a..b01263c4 100644 --- a/citar.el +++ b/citar.el @@ -41,8 +41,9 @@ (require 'subr-x)) (require 'seq) (require 'browse-url) +(require 'citar-cache) +(require 'citar-format) (require 'citar-file) -(require 'parsebib) (require 'crm) ;;; pre-1.0 API cleanup @@ -152,6 +153,19 @@ for the title field for new notes." :value-type string :options (main suffix preview note))) +(defcustom citar-ellipsis nil + "Ellipsis string to mark ending of truncated display fields. + +If t, use the value of `truncate-string-ellipsis`. If nil, no +ellipsis will be used. Otherwise, this should be a non-empty +string specifying the ellipsis." + :group 'citar + :type '(choice (const :tag "Use 'truncate-string-ellipsis'" t) + (const :tag "Disable ellipsis" nil) + (const "...") + (const "…") + (string :tag "Ellipsis string"))) + (defcustom citar-format-reference-function #'citar-format-reference "Function used to render formatted references. @@ -260,8 +274,7 @@ If nil, single resources will open without prompting." :group 'citar :type '(function)) -(defcustom citar-has-note-functions - '(citar--has-file-notes) +(defcustom citar-has-note-functions '(citar-file-has-notes) "Functions used for displaying note indicators. Such functions must take KEY and return non-nil when the @@ -411,156 +424,31 @@ When nil, all citar commands will use `completing-read`." ;;; Bibliography cache -(cl-defstruct (citar--bibliography - (:constructor citar--make-bibliography (filename)) - (:copier nil)) - "Cached bibliography file." - (filename - nil - :read-only t - :documentation - "True filename of a bibliography, as returned by `file-truename`.") - (hash - nil - :documentation - "Hash of the file's contents, as returned by `buffer-hash`.") - (buffers - nil - :documentation - "List of buffers that require this bibliography.") - (entries - (make-hash-table :test 'equal) - :documentation - "Hash table mapping citation keys to bibliography entries, - as returned by `parsebib-parse`.") - (preformatted - (make-hash-table :test 'equal) - :documentation - "Pre-formatted strings used to display bibliography entries; - see `citar--preformatter`.") - (format-string - nil - :documentation - "Format string used to generate pre-formatted strings.")) - -(defvar citar--bibliography-cache (make-hash-table :test 'equal) - "Cache for parsed bibliography files. -This is an association list following the pattern: - (FILE-ID . ENTRIES) -FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of -the bibliography file, and HASH a hash of its contents. -ENTRIES is a hash table with citation references as keys and fields alist as -values.") - -(defun citar--get-bibliography (filename &optional buffer) - "Return cached bibliography for FILENAME and associate it with BUFFER. -If FILENAME is not already cached, read and cache it. If BUFFER -is nil, use the current buffer. Otherwise, BUFFER should be a -buffer object or name that requires the bibliography FILENAME, or -a symbol like 'global." - (let* ((buffer (cond ((null buffer) (current-buffer)) - ((symbolp buffer) buffer) - (t (get-buffer buffer)))) - (cached (gethash filename citar--bibliography-cache)) - (bib (or cached (citar--make-bibliography filename)))) - (unless cached - (citar--update-bibliography bib) - (puthash filename bib citar--bibliography-cache)) - (cl-pushnew buffer (citar--bibliography-buffers bib)) - (unless (symbolp buffer) - (with-current-buffer buffer - (dolist (hook '(change-major-mode-hook kill-buffer-hook)) - (add-hook hook #'citar--release-bibliographies 0 'local)))) - bib)) - -(defun citar--cache-bibliographies (filenames &optional buffer) - "Return cached bibliographies for FILENAMES and associate them with BUFFER. -FILENAMES is a list of bibliography file names. If BUFFER is -nil, use the current buffer. Otherwise, BUFFER should be a -buffer object or name that requires these bibliographies, or a -symbol like 'global. - -Remove any existing associations between BUFFER and cached files -not included in FILENAMES. Release cached files that are no -longer needed by any other buffer. - -Return a list of `citar--bibliography` objects, one for each -element of FILENAMES." - (citar--release-bibliographies filenames buffer) - (mapcar - (lambda (filename) (citar--get-bibliography filename buffer)) - filenames)) - -(defun citar--release-bibliographies (&optional keep-filenames buffer) - "Dissociate BUFFER from cached bibliographies. -If BUFFER is nil, use the current buffer. Otherwise, BUFFER -should be a buffer object, buffer name, or a symbol like 'global. -KEEP-FILENAMES is a list of file names that are not dissociated -from BUFFER. - -Remove any bibliographies from the cache that are no longer -needed by any other buffer." - (let ((buffer (cond ((null buffer) (current-buffer)) - ((symbolp buffer) buffer) - (t (get-buffer buffer))))) - (maphash - (lambda (filename bib) - (unless (member filename keep-filenames) - (setf (citar--bibliography-buffers bib) - (delq buffer (citar--bibliography-buffers bib))) - (unless (citar--bibliography-buffers bib) - (citar--delete-bibliography-from-cache filename)))) - citar--bibliography-cache))) +(defun citar--bibliography-files (&rest buffers) + "Bibliography file names for BUFFERS. +The elements of BUFFERS are either buffers or the symbol 'global. +Returns the absolute file names of the bibliographies in all +these contexts. + +When BUFFERS is empty, return local bibliographies for the +current buffer and global bibliographies." + (citar-file--normalize-paths + (mapcan (lambda (buffer) + (if (eq buffer 'global) + (if (listp citar-bibliography) citar-bibliography + (list citar-bibliography)) + (with-current-buffer buffer + (citar--major-mode-function 'local-bib-files #'ignore)))) + (or buffers (list (current-buffer) 'global))))) (defun citar--bibliographies (&rest buffers) "Return bibliographies for BUFFERS." (delete-dups (mapcan (lambda (buffer) - (citar--cache-bibliographies (citar--bibliography-files buffer) buffer)) + (citar-cache--get-bibliographies (citar--bibliography-files buffer) buffer)) (or buffers (list (current-buffer) 'global))))) -(defun citar--delete-bibliography-from-cache (filename) - "Remove bibliography cache entry for FILENAME." - ;; TODO Perform other needed actions, like removing filenotify watches - (remhash filename citar--bibliography-cache)) - -(defun citar--update-bibliography (bib &optional force) - "Update the bibliography BIB from the original file. - -Unless FORCE is non-nil, the file is re-read only if it has been -modified since the last time BIB was updated." - (let* ((filename (citar--bibliography-filename bib)) - (entries (citar--bibliography-entries bib)) - (preformatted (citar--bibliography-preformatted bib)) - (formatstring (citar--bibliography-format-string bib)) - (newformatstring - (concat (propertize (citar--get-template 'main) 'face 'citar-highlight) - (propertize (citar--get-template 'suffix) 'face 'citar))) - (newhash - (with-temp-buffer - (insert-file-contents filename) - (buffer-hash)))) - ;; TODO Also check file size and modification time before hashing? - ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` - (unless (or force - (and (equal newhash (citar--bibliography-hash bib)) - (equal-including-properties formatstring newformatstring))) - ;; Update entries - (clrhash entries) - (parsebib-parse filename - :entries entries - :fields (citar--fields-to-parse)) - (setf (citar--bibliography-hash bib) newhash) - ;; Update preformatted strings - (clrhash preformatted) - (let ((preformatter (citar--preformatter newformatstring))) - (maphash - (lambda (citekey entry) - (puthash citekey (funcall preformatter entry) preformatted)) - entries)) - (setf (citar--bibliography-format-string bib) newformatstring)))) - ;;; Completion functions (defun citar--completion-table (candidates &optional filter &rest metadata) @@ -731,71 +619,34 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) -(defun citar--bibliography-files (&rest buffers) - "Bibliography file names for BUFFERS. -The elements of BUFFERS are either buffers or the symbol 'global. -Returns the absolute file names of the bibliographies in all -these contexts. - -When BUFFERS is empty, return local bibliographies for the -current buffer and global bibliographies." - (citar-file--normalize-paths - (mapcan (lambda (buffer) - (if (eq buffer 'global) - (if (listp citar-bibliography) citar-bibliography - (list citar-bibliography)) - (with-current-buffer buffer - (citar--major-mode-function 'local-bib-files #'ignore)))) - (or buffers (list (current-buffer) 'global))))) - (defun citar--ref-completion-table () "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and title of the reference. Values are the cite keys. Return nil if there are no bibliography files or no entries." ;; Populate bibliography cache. - (let* ((entries (citar--parse-bibliography)) - (hasnotep (citar-has-note)) - (hasfilep (citar-has-file)) - (mainwidth (citar--format-width (citar--get-template 'main))) - (suffixwidth (citar--format-width (citar--get-template 'suffix))) - (symbolswidth (string-width (citar--symbols-string t t t))) - (starwidth - (- (frame-width) (+ 2 symbolswidth mainwidth suffixwidth)))) - (cond - ((null entries) nil) ; no bibliography files - ;; if completion-cache is same as bibliography-cache, use the former - ((gethash entries citar--completion-cache) - citar--completion-cache) ; REVIEW ? - (t - (clrhash citar--completion-cache) - (dolist (key (citar--all-keys)) - (let* ((entry (citar--get-entry key)) - (hasfile - (when (funcall hasfilep key entry) "has:file")) - (hasnote - (when (funcall hasnotep key entry) "has:note")) - (candidatemain - (citar--format-entry - entry - starwidth - (citar--get-template 'main))) - (candidatesuffix - (citar--format-entry - entry - starwidth - (citar--get-template 'suffix))) - (invisible (concat hasfile " " hasnote)) - (completion - (string-trim-right - (concat - (propertize candidatemain 'face 'citar-highlight) " " - (propertize candidatesuffix 'face 'citar) - (propertize invisible 'invisible t))))) - (puthash completion key citar--completion-cache))) - (unless (map-empty-p citar--completion-cache) ; no key - (puthash entries t citar--completion-cache) ; REVIEW ? - citar--completion-cache))))) + (when-let ((bibs (citar--bibliographies))) + (let* ((entries (citar-cache--entries bibs)) + (preformatted (citar-cache--preformatted bibs)) + (fmtstr (citar--get-template 'completion)) + (hasnotep (citar-has-note)) + (hasfilep (citar-has-file)) + (hasnotetag (propertize " has:notes" 'invisible t)) + (hasfiletag (propertize " has:files" 'invisible t)) + (symbolswidth (string-width (citar--symbols-string t t t))) + (width (- (frame-width) symbolswidth 2)) + (format (citar-format--preformatted + fmtstr :width width :hide-elided t :ellipsis citar-ellipsis)) + (completions (make-hash-table :test 'equal))) + (maphash + (lambda (citekey preform) + (let* ((entry (gethash citekey entries)) + (cand (concat (string-trim-right (funcall format preform)) + (when (funcall hasnotep citekey entry) hasnotetag) + (when (funcall hasfilep citekey entry) hasfiletag)))) + (puthash cand citekey completions))) + preformatted) + completions))) (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. @@ -831,23 +682,12 @@ If no function is found, the DEFAULT function is called." (cdr bibliography))) results)) -(defun citar--get-entry (key &optional bibs) +(defun citar--get-entry (key) "Return entry for KEY, as an association list." - (catch :found - ;; Iterate through the cached bibliography hashes and find a key. - (dolist (bib (or bibs (citar--bibliographies))) - (let* ((entries (citar--bibliography-entries bib)) - (entry (gethash key entries))) - (when entry (throw :found entry)))) - nil)) - -(defun citar--get-entries (&optional bibs) - (let ((entries (make-hash-table :test 'equal))) - (dolist (bib (reverse (or bibs (citar--bibliographies)))) - (maphash (lambda (citekey entry) - (puthash citekey entry entries)) - (citar--bibliography-entries bib))) - entries)) + (citar-cache--entry key (citar--bibliographies))) + +(defun citar--get-entries () + (citar-cache--entries (citar--bibliographies))) (defun citar--get-value (field key-or-entry) "Return FIELD value for KEY-OR-ENTRY." @@ -991,11 +831,12 @@ repeatedly." (defun citar--get-template (template-name) "Return template string for TEMPLATE-NAME." - (let ((template - (cdr (assoc template-name citar-templates)))) - (unless template - (error "No template for \"%s\" - check variable 'citar-templates'" template-name)) - template)) + (or + (cdr (assq template-name citar-templates)) + (when (eq template-name 'completion) + (concat (propertize (citar--get-template 'main) 'face 'citar-highlight) + (propertize (citar--get-template 'suffix) 'face 'citar))) + (error "No template for \"%s\" - check variable 'citar-templates'" template-name))) (defun citar--all-keys () "List all keys available in current bibliography." @@ -1025,162 +866,6 @@ repeatedly." (search (completing-read "Preset: " citar-presets))) (insert search))) -;;; Formatting functions - -(defun citar--format-parse (format-string) - "Parse FORMAT-STRING." - (let ((regex (concat "\\${" ; ${ - "\\(.*?\\)" ; field names - "\\(?::[[:blank:]]*" ; : + space - "\\(.*?\\)" ; format spec - "[[:blank:]]*\\)?}")) ; space + } - (position 0) - (parsed nil)) - (while (string-match regex format-string position) - (let* ((begin (match-beginning 0)) - (end (match-end 0)) - (textprops (text-properties-at begin format-string)) - (fields (match-string-no-properties 1 format-string)) - (spec (match-string-no-properties 2 format-string)) - (width (cond - ((null spec) nil) - ((equal spec "*") '*) - (t (string-to-number spec))))) - (when (< position begin) - (push (substring format-string position begin) parsed)) - (setq position end) - (push (cons (nconc (when width `(:width ,width)) - (when textprops `(:text-properties ,textprops))) - (split-string-and-unquote fields)) - parsed))) - (when (< position (length format-string)) - (push (substring format-string position) parsed)) - (nreverse parsed))) - -(defun citar--preformat-parse (format-string) - "Parse and group FORMAT-STRING." - (let ((parsed (citar--format-parse format-string)) - groups group props) - (dolist (field parsed) - (unless props (setq props (list :width 0))) - (let ((fieldwidth (cond - ((stringp field) (string-width field)) - ((listp field) (plist-get (car field) :width))))) - (cond - ((eq fieldwidth '*) - (push (cons props (nreverse group)) groups) - (setcar field (plist-put (car field) :width nil)) - (push (cons '(:width *) (list field)) groups) - (setq group nil props nil)) - ((numberp fieldwidth) - (push field group) - (if-let* ((oldwidth (plist-get props :width)) - ((numberp oldwidth))) - (setq props (plist-put props :width (+ oldwidth fieldwidth))))) - (t - (push field group) - (setq props (plist-put props :width nil)))))) - (when group - (push (cons props (nreverse group)) groups)) - (nreverse groups))) - -(defun citar--preformatter (format-string) - "Preformat according to FORMAT-STRING." - (let ((groups (citar--preformat-parse format-string))) - (lambda (entry) - (mapcar - (pcase-lambda (`(,props . ,group)) - (citar--format-string-with-props - props - (mapconcat - (lambda (field) - (if (stringp field) - field - (apply #'citar--format-entry-with-props entry field))) - group ""))) - groups)))) - -(defun citar--format-preformatted (format-string) - (let* ((groups (citar--preformat-parse format-string)) - (widths (mapcar (lambda (group) (plist-get (car group) :width)) - groups)) - (nstars (cl-count '* ))) - (lambda (preformatted)))) - -(defun citar--format-string-with-props (props string) - (let ((width (plist-get props :width)) - (textprops (plist-get props :text-properties))) - (when textprops - (setq string (apply #'propertize string textprops))) - (when (numberp width) - (setq string (truncate-string-to-width string width 0 ?\s ))) - string)) - -(defun citar--format-entry-with-props (entry props &rest fields) - "Format FIELDS of ENTRY using PROPS." - (citar--format-string-with-props props (citar--display-value fields entry))) - -(defun citar--format-width (format-string) - "Calculate minimal width needed by the FORMAT-STRING." - (let ((content-width (apply #'+ - (seq-map #'string-to-number - (split-string format-string ":")))) - (whitespace-width (string-width (citar--format format-string - (lambda (_) ""))))) - (+ content-width whitespace-width))) - -(defun citar--fit-to-width (value width) - "Propertize the string VALUE so that only the WIDTH columns are visible." - (let* ((truncated-value (truncate-string-to-width value width)) - (display-value (truncate-string-to-width truncated-value width 0 ?\s))) - (if (> (string-width value) width) - (concat display-value (propertize (substring value (length truncated-value)) - 'invisible t)) - display-value))) - -(defun citar--format (template replacer) - "Format TEMPLATE with the function REPLACER. -The templates are of form ${foo} for variable foo. -REPLACER takes an argument of the format variable. -Adapted from `org-roam-format-template'." - (replace-regexp-in-string - "\\${\\([^}]+\\)}" - (lambda (md) - (save-match-data - (if-let ((text (funcall replacer (match-string 1 md)))) - text - (signal 'citar-format-resolve md)))) - template - ;; Need literal to make sure it works - t t)) - -(defun citar--format-entry (entry width format-string) - "Formats a BibTeX ENTRY for display in results list. -WIDTH is the width for the * field, and the display format is governed by -FORMAT-STRING." - (citar--format - format-string - (lambda (raw-field) - (let* ((field (split-string raw-field ":")) - (field-names (split-string (car field) "[ ]+")) - (field-width (string-to-number (cadr field))) - (display-width (if (> field-width 0) - ;; If user specifies field width of "*", use - ;; WIDTH; else use the explicit 'field-width'. - field-width - width)) - ;; Make sure we always return a string, even if empty. - (display-value (citar--display-value field-names entry))) - (citar--fit-to-width display-value display-width))))) - -(defun citar--format-entry-no-widths (entry format-string) - "Format ENTRY for display per FORMAT-STRING." - (citar--format - format-string - (lambda (raw-field) - (let ((field-names (split-string raw-field "[ ]+"))) - (citar--display-value field-names entry))))) - ;;; At-point functions for Embark ;;;###autoload @@ -1457,14 +1142,12 @@ ARG is forwarded to the mode-specific insertion function given in (defun citar-format-reference (keys) "Return formatted reference(s) for the elements of KEYS." - (let* ((template (citar--get-template 'preview)) - (references - (with-temp-buffer - (dolist (key keys) - (when template - (insert (citar--format-entry-no-widths key template)))) - (buffer-string)))) - references)) + (let* ((entries (mapcar #'citar--get-entry keys)) + (template (citar--get-template 'preview))) + (with-temp-buffer + (dolist (entry entries) + (insert (citar-format--entry template entry))) + (buffer-string)))) ;;;###autoload (defun citar-insert-keys (keys) From 93c8452fe83726c3b8f719b09d53061c315d5f4a Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 13 Jun 2022 16:13:58 -0600 Subject: [PATCH 22/78] Remove citar-get-data-entries and citar-all-keys. Do we really need these functions? There are probably better ways of implementing any functionality that would use these. --- citar.el | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/citar.el b/citar.el index b01263c4..393cd283 100644 --- a/citar.el +++ b/citar.el @@ -669,19 +669,6 @@ If no function is found, the DEFAULT function is called." ;; Data access functions -(cl-defun citar-get-data-entries (&optional &key filter) - "Return a subset of entries in bibliography by FILTER. - - (citar-get-data-entries :filter (citar-has-note))" - (let ((results (make-hash-table :test #'equal))) - (dolist (bibliography citar--bibliography-cache) - (maphash - (lambda (citekey entry) - (when (funcall filter citekey) - (puthash citekey entry results))) - (cdr bibliography))) - results)) - (defun citar--get-entry (key) "Return entry for KEY, as an association list." (citar-cache--entry key (citar--bibliographies))) @@ -838,12 +825,6 @@ repeatedly." (propertize (citar--get-template 'suffix) 'face 'citar))) (error "No template for \"%s\" - check variable 'citar-templates'" template-name))) -(defun citar--all-keys () - "List all keys available in current bibliography." - (seq-mapcat (pcase-lambda (`(,_ . ,entries)) - (map-keys entries)) - (citar--parse-bibliography))) - (defun citar--get-link (entry) "Return a link for an ENTRY." (let* ((field (citar--field-with-value '(doi pmid pmcid url) entry)) From 9581206beb686a820f094e9d29ac140e05c339eb Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 14 Jun 2022 07:00:20 -0400 Subject: [PATCH 23/78] Update CONTRIBUTING.org --- CONTRIBUTING.org | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org index df68cb37..14bfb9d4 100644 --- a/CONTRIBUTING.org +++ b/CONTRIBUTING.org @@ -11,10 +11,10 @@ If you would like to contribute, details: ** Basic Architecture -Citar has two primary caches, each of which store the data in hash tables: +Citar uses a cache, which stores two hash tables for each bibliography file: -- bibliographic :: keys are citekeys, values are alists of entry fields -- completion :: keys are completion strings, values are citekeys +- entries :: as returned by =parsebib-parse=, keys are citekeys, values are alists of entry fields +- pre-formatted :: values are partially-formatted completion strings The =citar--ref-completion-table= function returns a hash table from the bibliographic cache, and ~citar--get-entry~ and ~-citar--get-value~ provide access to those data. Most user-accessible citar functions take an argument ~key~ or ~keys~. From f9393df90f1a074b17518bd9da0d55485faa18d3 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 09:39:23 -0600 Subject: [PATCH 24/78] Update citar-capf.el for new API; still doesn't work for some reason. --- citar-capf.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/citar-capf.el b/citar-capf.el index c6dc6d4c..cb17b154 100644 --- a/citar-capf.el +++ b/citar-capf.el @@ -25,10 +25,10 @@ (declare-function org-element-type "ext:org-element") (declare-function org-element-context "ext:org-element") ;; Declare function from citar -;; (declare-function citar--ref-completion-table "citar") ;; pending cache revisions +(declare-function citar--ref-completion-table "citar") ;; pending cache revisions ;; Define vars for capf -(defvar citar-capf--candidates (or (citar--get-candidates) +(defvar citar-capf--candidates (or (citar--ref-completion-table) (user-error "No bibliography set")) "Completion candidates for `citar-capf'.") @@ -41,7 +41,7 @@ (defun citar-capf--exit (str _status) "Return key for STR from CANDIDATES hash." (delete-char (- (length str))) - (insert (cadr (assoc str citar-capf--candidates)))) + (insert (gethash str citar-capf--candidates))) ;;;; Citar-Capf ;;;###autoload From 4ab16189b9e91e3d147ef0df50e3500fec84fabf Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 10:25:01 -0600 Subject: [PATCH 25/78] Fix `citar-open-entry` Co-authored-by: Bruce D'Arcus --- citar.el | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/citar.el b/citar.el index 393cd283..5ee2e3ff 100644 --- a/citar.el +++ b/citar.el @@ -1014,11 +1014,7 @@ With prefix, rebuild the cache before offering candidates." "Open bibliographic entry associated with the KEY. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) - (when-let ((bibtex-files - (seq-concatenate - 'list - citar-bibliography - (citar--local-files-to-cache)))) + (when-let ((bibtex-files (citar--bibliography-files)) (bibtex-search-entry (car key) t nil t))) ;;;###autoload From 073b8e8a848d073c600397a8cce6dba7f414be72 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 13:33:53 -0600 Subject: [PATCH 26/78] Fix missing closing paren --- citar.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/citar.el b/citar.el index 5ee2e3ff..09a388f6 100644 --- a/citar.el +++ b/citar.el @@ -1014,7 +1014,7 @@ With prefix, rebuild the cache before offering candidates." "Open bibliographic entry associated with the KEY. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) - (when-let ((bibtex-files (citar--bibliography-files)) + (when-let ((bibtex-files (citar--bibliography-files))) (bibtex-search-entry (car key) t nil t))) ;;;###autoload From 243984cd52334de4365878261401ebec8e904c2a Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 14 Jun 2022 09:41:48 -0600 Subject: [PATCH 27/78] Improve the performance of formatting functions. Also add tests for `citar-format--star-widths` --- citar-cache.el | 26 ++++-- citar-format.el | 192 +++++++++++++++----------------------- citar.el | 48 ++++++---- test/citar-format-test.el | 59 ++++++++++++ 4 files changed, 183 insertions(+), 142 deletions(-) create mode 100644 test/citar-format-test.el diff --git a/citar-cache.el b/citar-cache.el index 784ba4b0..ef3839ab 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -31,6 +31,7 @@ (declare-function citar--get-template "citar") (declare-function citar--fields-to-parse "citar") +(defvar citar-ellipsis) ;;; Variables: @@ -189,7 +190,7 @@ modified since the last time BIB was updated." (buffer-hash)))) ;; TODO Also check file size and modification time before hashing? ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` - (unless (or force (equal newhash (citar-cache--bibliography-hash bib))) + (when (or force (not (equal newhash (citar-cache--bibliography-hash bib)))) ;; Update entries (clrhash entries) (parsebib-parse filename :entries entries :fields (citar--fields-to-parse)) @@ -201,13 +202,26 @@ modified since the last time BIB was updated." (defun citar-cache--preformat-bibliography (bib) "Updated pre-formatted strings in BIB." (let* ((entries (citar-cache--bibliography-entries bib)) - (fmtstr (citar-cache--bibliography-format-string bib)) - (preformat (citar-format--preformat fmtstr :hide-elided t)) + (formatstr (citar-cache--bibliography-format-string bib)) + (fieldspecs (citar-format--parse formatstr)) (preformatted (citar-cache--bibliography-preformatted bib))) (clrhash preformatted) - (maphash (lambda (citekey entry) - (puthash citekey (funcall preformat entry) preformatted)) - entries))) + (maphash + (lambda (citekey entry) + (let* ((preformat (citar-format--preformat fieldspecs entry + t citar-ellipsis)) + ;; CSL-JSOM lets citekey be an arbitrary string. Quote it if... + (keyquoted (if (or (string-empty-p citekey) ; ... it's empty, + (= ?\" (aref citekey 0)) ; ... starts with ", + (cl-find ?\s citekey)) ; ... or has a space + (prin1-to-string citekey) + citekey)) + (prefix (propertize (concat keyquoted (when (cdr preformat) " ")) + 'invisible t))) + (setcdr preformat (cons (concat prefix (cadr preformat)) + (cddr preformat))) + (puthash citekey preformat preformatted))) + entries))) ;;; Utility functions: diff --git a/citar-format.el b/citar-format.el index 73f8eabe..5495b929 100644 --- a/citar-format.el +++ b/citar-format.el @@ -27,64 +27,63 @@ (require 'cl-lib)) (declare-function citar--display-value "citar") -(defvar citar-ellipsis) ;;; Formatting bibliography entries -(cl-defun citar-format--entry (format-string entry &key width hide-elided - (ellipsis citar-ellipsis)) +(cl-defun citar-format--entry (format-string entry &optional width + &key hide-elided ellipsis) "Format ENTRY according to FORMAT-STRING." - (cl-flet ((getwidth (fieldspec) - (unless (stringp fieldspec) - (plist-get (car fieldspec) :width))) - (fmtfield (fieldspec) - (citar-format--fieldspec fieldspec entry - :hide-elided hide-elided - :ellipsis ellipsis))) - (let* ((fieldspecs (citar-format--parse format-string)) - (widths (mapcar #'getwidth fieldspecs)) - (strings (mapcar #'fmtfield fieldspecs))) - (citar-format--star-widths widths strings :width width - :hide-elided hide-elided :ellipsis ellipsis)))) + (let* ((fieldspecs (citar-format--parse format-string)) + (preform (citar-format--preformat fieldspecs entry + hide-elided ellipsis))) + (if width + (citar-format--star-widths (- width (car preform)) (cdr preform) + hide-elided ellipsis) + (apply #'concat (cdr preform))))) ;;; Pre-formatting bibliography entries -(cl-defun citar-format--preformat (format-string &key hide-elided - (ellipsis citar-ellipsis)) - "Preformat according to FORMAT-STRING. -See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." - (let ((fieldgroups (citar-format--preformat-parse format-string))) - (lambda (entry) - (cl-flet ((fmtfield (fieldspec) - (citar-format--fieldspec fieldspec entry - :hide-elided hide-elided - :ellipsis ellipsis))) - (mapcar (lambda (groupspec) - (mapconcat #'fmtfield (cdr groupspec) "")) - fieldgroups))))) - - -(cl-defun citar-format--preformatted (format-string &key width hide-elided - (ellipsis citar-ellipsis)) - "Fit pre-formatted strings to WIDTH according to FORMAT-STRING. -See `citar-format--string for the meaning of HIDE-ELIDED and ELLIPSIS." - (let* ((fieldgroups (citar-format--preformat-parse format-string)) - (widths (mapcar (lambda (groupspec) - (plist-get (car groupspec) :width)) - fieldgroups))) - (lambda (preformatted) - (citar-format--star-widths widths preformatted :width width - :hide-elided hide-elided :ellipsis ellipsis)))) +(defun citar-format--preformat (fieldspecs entry hide-elided ellipsis) + (let ((preformatted nil) + (fields "") + (width 0)) + (dolist (fieldspec fieldspecs) + (pcase fieldspec + ((pred stringp) + (cl-callf concat fields fieldspec) + (cl-incf width (string-width fieldspec))) + (`(,props . ,fieldnames) + (let* ((fieldwidth (plist-get props :width)) + (textprops (plist-get props :text-properties)) + (value (citar--display-value fieldnames entry)) + (display (citar-format--string value + :width fieldwidth + :text-properties textprops + :hide-elided hide-elided + :ellipsis ellipsis))) + (cond + ((eq '* fieldwidth) + (push fields preformatted) + (setq fields "") + (push display preformatted)) + (t + (cl-callf concat fields display) + (cl-incf width (if (numberp fieldwidth) + fieldwidth + (string-width value))))))))) + (unless (string-empty-p fields) + (push fields preformatted)) + (cons width (nreverse preformatted)))) ;;; Internal implementation functions -(cl-defun citar-format--fieldspec (fieldspec entry &key hide-elided ellipsis) +(defun citar-format--fieldspec (fieldspec entry hide-elided ellipsis) "Format FIELDSPEC using information from ENTRY. See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." (if (stringp fieldspec) @@ -97,7 +96,7 @@ See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." fmtprops)))) -(cl-defun citar-format--string (string +(cl-defsubst citar-format--string (string &key width text-properties hide-elided ellipsis) "Truncate STRING to WIDTH and apply TEXT-PROPERTIES. If HIDE-ELIDED is non-nil, the truncated part of STRING is @@ -110,38 +109,38 @@ display instead of the truncated part of the text." (setq string (truncate-string-to-width string width 0 ?\s ellipsis hide-elided))) string) - -(cl-defun citar-format--star-widths (widths strings &key width - hide-elided ellipsis) - "Format STRINGS according to WIDTHS to fit WIDTH." - (if (not (and (numberp width) (cl-find '* widths))) - ;; If width is unlimited or there are no *-width fields, just join strings. - ;; We only support truncating *-width fields. - (string-join strings) - ;; Otherwise, calculate extra space available for *-width fields - (let ((usedwidth 0) (nstars 0)) - ;; For fields without width spec, add their actual width to usedwidth - (cl-mapc - (lambda (width string) - (cond ((eq '* width) (cl-incf nstars)) - ((numberp width) (cl-incf usedwidth width)) - ((null width) (cl-incf usedwidth (string-width string))))) - widths strings) - (let* ((extrawidth (max 0 (- width usedwidth))) - (starwidth (/ extrawidth nstars)) - (remainder (% extrawidth nstars)) - (starindex 0)) - (string-join - (cl-mapcar - (lambda (width string) - (if (not (eq width '*)) - string - (cl-incf starindex) - (citar-format--string - string - :width (+ starwidth (if (<= starindex remainder) 1 0)) - :hide-elided hide-elided :ellipsis ellipsis))) - widths strings)))))) +(defun citar-format--star-widths (alloc strings &optional hide-elided ellipsis) + "Concatenate STRINGS and truncate every other element to fit in ALLOC. +Use this function along with `citar-format--preformat' to fit a +formatted string to a desired display width; see +`citar-format--entry' for how to do this. + +Return a string consisting of the concatenated elements of +STRINGS. The odd-numbered elements are included as-is, while the +even-numbered elements are padded or truncated to a total width +of ALLOC, which must be an integer. All these odd-numbered +elements are allocated close-to-equal widths. + +Perform the truncation using `citar-format--string', which see +for the meaning of HIDE-ELIDED and ELLIPSIS." + (let ((nstars (/ (length strings) 2))) + (if (= 0 nstars) + (or (car strings) "") + (cl-loop + with alloc = (max 0 alloc) + with starwidth = (/ alloc nstars) + with remainder = (% alloc nstars) + with formatted = (car strings) + for (starstring following) on (cdr strings) by #'cddr + for nthstar from 1 + do (let* ((starwidth (if (> nthstar remainder) starwidth + (1+ starwidth))) + (starstring (citar-format--string + starstring + :width starwidth + :hide-elided hide-elided :ellipsis ellipsis))) + (cl-callf concat formatted starstring following)) + finally return formatted)))) ;;; Parsing format strings @@ -178,48 +177,5 @@ display instead of the truncated part of the text." (nreverse fieldspecs))) -(defun citar-format--preformat-parse (format-string) - "Parse and group FORMAT-STRING." - (let (fieldgroups group (groupwidth 0)) - (cl-flet ((newgroup () - (when group - (push (cons (when groupwidth `(:width ,groupwidth)) - (nreverse group)) - fieldgroups) - (setq group nil)) - (setq groupwidth 0))) - (dolist (fieldspec (citar-format--parse format-string)) - (let ((fieldwidth (cond - ((stringp fieldspec) (string-width fieldspec)) - ((listp fieldspec) (plist-get (car fieldspec) :width))))) - (cond - ((eq fieldwidth '*) - ;; *-width field; start a new group - (newgroup) - ;; Pre-format the field at unlimited width by setting :width to nil - (cl-callf plist-put (car fieldspec) :width nil) - ;; Add the field in its own pre-format group with :width * - (push fieldspec group) - (setq groupwidth '*) - (newgroup)) - ((numberp fieldwidth) - ;; Fixed-length field; start new group if needed - (unless (numberp groupwidth) - (newgroup)) - ;; Add field to group and increment group width - (push fieldspec group) - (cl-incf groupwidth fieldwidth)) - (t - ;; Unknown-length field; start new group if needed - (unless (null groupwidth) - (newgroup)) - ;; Add field to group; group width is now unknown - (push fieldspec group) - (setq groupwidth nil))))) - ;; Add any remaining fields to group - (newgroup)) - (nreverse fieldgroups))) - - (provide 'citar-format) ;;; citar-format.el ends here diff --git a/citar.el b/citar.el index 09a388f6..8ae94a35 100644 --- a/citar.el +++ b/citar.el @@ -641,13 +641,31 @@ Return nil if there are no bibliography files or no entries." (maphash (lambda (citekey preform) (let* ((entry (gethash citekey entries)) - (cand (concat (string-trim-right (funcall format preform)) - (when (funcall hasnotep citekey entry) hasnotetag) - (when (funcall hasfilep citekey entry) hasfiletag)))) + (starswidth (- width (car preform))) + (strings (cdr preform)) + (display (citar-format--star-widths + starswidth strings t citar-ellipsis)) + ;; (hasfile (and hasfilep (funcall hasfilep citekey entry))) + (hasnote (and hasnotep (funcall hasnotep citekey entry))) + (hasfile t) + ;; (hasnote t) + (cand (if (not (or hasfile hasnote)) display + (concat display + (when hasnote hasnotetag) + (when hasfile hasfiletag))))) (puthash cand citekey completions))) preformatted) completions))) +(defun citar--extract-candidate-citekey (candidate) + "Extract the citation key from string CANDIDATE." + (unless (string-empty-p candidate) + (if (= ?\" (aref candidate 0)) + (read candidate) + (substring-no-properties candidate 0 (cl-position ?\s candidate))))) + +;;; Major-mode functions + (defun citar--get-major-mode-function (key &optional default) "Return function associated with KEY in `major-mode-functions'. If no function is found matching KEY for the current major mode, @@ -667,7 +685,7 @@ return DEFAULT." If no function is found, the DEFAULT function is called." (apply (citar--get-major-mode-function key default) args)) -;; Data access functions +;;; Data access functions (defun citar--get-entry (key) "Return entry for KEY, as an association list." @@ -722,10 +740,8 @@ personal names of the form \"family, given\"." (defun citar--fields-for-format (template) "Return list of fields for TEMPLATE." - (let* ((regexp "\\(?:\\`\\|}\\|:\\)[^{]*\\(?:\\${\\|\\'\\)\\|[[:space:]]+")) - ;; The readable version of regexp is: - ;; (rx (or (seq (or bos "}" ":") (0+ (not "{")) (or "${" eos)) (1+ space))) - (split-string template regexp t))) + (mapcan (lambda (fieldspec) (when (consp fieldspec) (cdr fieldspec))) + (citar-format--parse template))) (defun citar--fields-in-formats () "Find the fields to mentioned in the templates." @@ -737,11 +753,10 @@ personal names of the form \"family, given\"." (defun citar--fields-to-parse () "Determine the fields to parse from the template." - (seq-concatenate - 'list - (citar--fields-in-formats) - (list citar-file-variable) - citar-additional-fields)) + (delete-dups (append (citar--fields-in-formats) + (when citar-file-variable + (list citar-file-variable)) + citar-additional-fields))) (defun citar-has-file () "Return predicate testing whether entry has associated files. @@ -869,10 +884,7 @@ repeatedly." (defun citar--reference-transformer (type target) "Look up key for a citar-reference TYPE and TARGET." - (cons type (or (cadr (assoc target - (with-current-buffer (embark--target-buffer) - ;; FIX how? - (citar--get-candidates))))))) + (cons type (citar--extract-candidate-citekey target))) (defun citar--embark-selected () "Return selected candidates from `citar--select-multiple' for embark." @@ -1028,7 +1040,7 @@ With prefix, rebuild the cache before offering candidates." (defun citar--insert-bibtex (key) "Insert the bibtex entry for KEY at point." (let* ((bibtex-files - (seq-concatenate 'list citar-bibliography (citar--local-files-to-cache))) + (citar--bibliography-files)) (entry (with-temp-buffer (bibtex-set-dialect) diff --git a/test/citar-format-test.el b/test/citar-format-test.el new file mode 100644 index 00000000..ddeba9b9 --- /dev/null +++ b/test/citar-format-test.el @@ -0,0 +1,59 @@ +;;; citar-format-test.el --- Tests for citar-format.el -*- lexical-binding: t; -*- + +;;; Commentary: + +;; + +;;; Code: + +(require 'ert) +(require 'seq) +(require 'citar-format) + +(ert-deftest citar-format-test--star-widths () + "Test `citar-format--star-widths`." + + (should (string-empty-p (citar-format--star-widths 80 nil))) + + ;; For single string, return the original string; not a copy + (let ((strings '("foo"))) + (should (eq (car strings) (citar-format--star-widths 80 strings)))) + + (let ((strings '("foo" "bar" "baz"))) + (should (equal "foobaz" (citar-format--star-widths 0 strings))) + (should (equal "foobabaz" (citar-format--star-widths 2 strings))) + (should (equal "foob…baz" (citar-format--star-widths 2 strings nil "…"))) + (should (equal "foobarbaz" (citar-format--star-widths 3 strings))) + (should (equal "foobar baz" (citar-format--star-widths 4 strings))) + + ;; When hide-elided is t, the actual string contents should be equal + (cl-loop for w from 0 to 3 + do (should (equal "foobarbaz" (citar-format--star-widths w strings t)))) + ;; ...unless the allocated width is greater than the string length + (should (equal "foobar baz" (citar-format--star-widths 4 strings))) + + ;; When hide-elided is t, the hidden text should have the 'display "" + ;; property. N.B. equal-including-properties is slightly broken; see + ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=6581 + (should (ert-equal-including-properties #("foobarbaz" 5 6 (display "")) + (citar-format--star-widths 2 strings t))) + + ;; Test with ellipsis + (should (ert-equal-including-properties #("foobarbaz" 4 6 (display "…")) + (citar-format--star-widths 2 strings t "…")))) + + (let ((strings '("foo" "bar" "baz" "qux"))) + (should (equal "foobaz" (citar-format--star-widths 0 strings))) + (should (equal "foobbaz" (citar-format--star-widths 1 strings))) + (should (equal "foobbazq" (citar-format--star-widths 2 strings))) + (should (equal "foobabazq" (citar-format--star-widths 3 strings))) + (should (equal "foobabazqu" (citar-format--star-widths 4 strings))) + + ;; Test with ellipsis + (should (equal "foob…baz…" (citar-format--star-widths 3 strings nil "…"))) + (should (ert-equal-including-properties + #("foobarbazqux" 4 6 (display "…") 9 12 (display "…")) + (citar-format--star-widths 3 strings t "…"))))) + +(provide 'citar-format-test) +;;; citar-format-test.el ends here From da382a80013fcc5ce2b6ee477004c03403b29a3a Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 17 Jun 2022 16:10:10 -0600 Subject: [PATCH 28/78] Use `map-merge` to merge hash tables --- citar-cache.el | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/citar-cache.el b/citar-cache.el index ef3839ab..4be85b8e 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -27,6 +27,7 @@ (require 'cl-lib)) (require 'parsebib) (require 'citar-format) +(require 'map) (declare-function citar--get-template "citar") (declare-function citar--fields-to-parse "citar") @@ -110,12 +111,12 @@ element of FILENAMES." nil)) (defun citar-cache--entries (bibs) - (citar-cache--merge-hash-tables - (mapcar #'citar-cache--bibliography-entries bibs))) + (apply #'map-merge '(hash-table :test equal) + (nreverse (mapcar #'citar-cache--bibliography-entries bibs)))) (defun citar-cache--preformatted (bibs) - (citar-cache--merge-hash-tables - (mapcar #'citar-cache--bibliography-preformatted bibs))) + (apply #'map-merge '(hash-table :test equal) + (nreverse (mapcar #'citar-cache--bibliography-preformatted bibs)))) ;;; Creating and deleting bibliography caches @@ -238,16 +239,5 @@ Otherwise, return the buffer object whose name is BUFFER." (t (get-buffer buffer)))) -(defun citar-cache--merge-hash-tables (hash-tables) - "Merge hash tables in HASH-TABLES." - (when-let ((hash-tables (reverse hash-tables)) - (first (pop hash-tables))) - (if (null hash-tables) - first - (let ((result (copy-hash-table first))) - (dolist (table hash-tables result) - (maphash (lambda (key entry) (puthash key entry result)) table)))))) - - (provide 'citar-cache) ;;; citar-cache.el ends here From 86eca4f98d23351b34da9e313c0875e2342783f2 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 14:28:40 -0400 Subject: [PATCH 29/78] Refactor citar-key-finder Close #642 --- citar.el | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/citar.el b/citar.el index ffd23082..21b47743 100644 --- a/citar.el +++ b/citar.el @@ -920,6 +920,10 @@ predicate, return it." ;;;###autoload (defun citar-key-finder () + "Return ctiation key as string at point." + (cadr (citar--key-finder))) + +(defun citar--key-finder () "Return the citation key at point." (when-let (key (and (not (minibufferp)) (citar--major-mode-function 'key-at-point #'ignore))) From 8f58ad8f51c34b1fed3ae794cfc69e1410180928 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 17 Jun 2022 18:20:15 -0600 Subject: [PATCH 30/78] Speed up has-file indicator --- citar-file.el | 57 ++++++---------------- citar.el | 131 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 80 deletions(-) diff --git a/citar-file.el b/citar-file.el index 30d2d717..97fbb240 100644 --- a/citar-file.el +++ b/citar-file.el @@ -232,44 +232,6 @@ need to scan the contents of DIRS in this case." (puthash key (nreverse filelist) files)) files)))) -(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: - -- 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`. - -- 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 &optional entry) - (let* ((nentry (or entry (citar--get-entry key))) - (xref (citar--get-value "crossref" nentry)) - (cached (if (and xref - (not (eq 'unknown (gethash xref files 'unknown)))) - (gethash xref files 'unknown) - (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 nentry entry-field dirs extensions)) - files)))))) - (defun citar-file--files-for-entry (key entry dirs extensions) "Find files related to bibliography item KEY with metadata ENTRY. See `citar-file--files-for-multiple-entries` for details on DIRS, @@ -326,10 +288,21 @@ of files found in two ways: nil 0 nil file))) -(defun citar-file-has-notes () - "Return a predicate testing whether a reference has associated notes." - (citar-file--has-file citar-notes-paths - citar-file-note-extensions)) +(defun citar-file-has-library-files (&optional _entries) + "Return predicate testing whether cite key has library files." + (let ((files (citar-file--directory-files + citar-library-paths nil citar-library-file-extensions + citar-file-additional-files-separator))) + (lambda (key) + (gethash key files)))) + +(defun citar-file-has-notes (&optional _entries) + "Return predicate testing whether cite key has associated notes." + (let ((files (citar-file--directory-files + citar-notes-paths nil citar-file-note-extensions + citar-file-additional-files-separator))) + (lambda (key) + (gethash key files)))) (defun citar-file--open-note (key entry) "Open a note file from KEY and ENTRY." diff --git a/citar.el b/citar.el index 8ae94a35..a0aaabc6 100644 --- a/citar.el +++ b/citar.el @@ -40,6 +40,7 @@ (require 'cl-lib) (require 'subr-x)) (require 'seq) +(require 'map) (require 'browse-url) (require 'citar-cache) (require 'citar-format) @@ -109,6 +110,12 @@ :group 'citar :type '(repeat file)) +(defcustom citar-has-file-functions '(citar-has-file-field + citar-file-has-library-files) + "List of functions to test if an entry has associated files." + :group 'citar + :type '(repeat function)) + (defcustom citar-library-paths nil "A list of files paths for related PDFs, etc." :group 'citar @@ -129,7 +136,18 @@ When nil, the function will not filter the list of files." :group 'citar :type '(repeat directory)) -(defcustom citar-additional-fields '("doi" "url" "crossref") +(defcustom citar-crossref-variable "crossref" + "The bibliography field to look for cross-referenced entries. + +When non-nil, find associated files and notes not only in the +original entry, but also in entries specified in the field named +by this variable." + :group 'citar + :type '(choice (const "crossref") + (string :tag "Field name") + (const :tag "Ignore cross-references" nil))) + +(defcustom citar-additional-fields '("doi" "url") "A list of fields to add to parsed data. By default, citar filters parsed data based on the fields @@ -619,42 +637,38 @@ HISTORY is the `completing-read' history argument." ((string-match "http" resource 0) "Links") (t "Library Files"))))) -(defun citar--ref-completion-table () +(cl-defun citar--ref-completion-table (&optional (bibs (citar--bibliographies)) + (entries (citar-cache--entries bibs))) "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and title of the reference. Values are the cite keys. Return nil if there are no bibliography files or no entries." ;; Populate bibliography cache. - (when-let ((bibs (citar--bibliographies))) - (let* ((entries (citar-cache--entries bibs)) - (preformatted (citar-cache--preformatted bibs)) - (fmtstr (citar--get-template 'completion)) - (hasnotep (citar-has-note)) - (hasfilep (citar-has-file)) + (when bibs + (let* ((preformatted (citar-cache--preformatted bibs)) + (hasnotep (citar-has-notes-for-entries entries)) + (hasfilep (citar-has-files-for-entries entries)) (hasnotetag (propertize " has:notes" 'invisible t)) (hasfiletag (propertize " has:files" 'invisible t)) (symbolswidth (string-width (citar--symbols-string t t t))) (width (- (frame-width) symbolswidth 2)) - (format (citar-format--preformatted - fmtstr :width width :hide-elided t :ellipsis citar-ellipsis)) - (completions (make-hash-table :test 'equal))) + (completions (make-hash-table :test 'equal :size (hash-table-count entries)))) (maphash - (lambda (citekey preform) - (let* ((entry (gethash citekey entries)) - (starswidth (- width (car preform))) - (strings (cdr preform)) + (lambda (citekey _entry) + (let* ((hasfile (and hasfilep (funcall hasfilep citekey))) + (hasnote (and hasnotep (funcall hasnotep citekey))) + (preform (or (gethash citekey preformatted) + (error "No preformatted candidate string: %s" citekey))) (display (citar-format--star-widths - starswidth strings t citar-ellipsis)) - ;; (hasfile (and hasfilep (funcall hasfilep citekey entry))) - (hasnote (and hasnotep (funcall hasnotep citekey entry))) - (hasfile t) - ;; (hasnote t) - (cand (if (not (or hasfile hasnote)) display - (concat display - (when hasnote hasnotetag) - (when hasfile hasfiletag))))) - (puthash cand citekey completions))) - preformatted) + (- width (car preform)) (cdr preform) + t citar-ellipsis)) + (tagged (if (not (or hasfile hasnote)) + display + (concat display + (when hasnote hasnotetag) + (when hasfile hasfiletag))))) + (puthash tagged citekey completions))) + entries) completions))) (defun citar--extract-candidate-citekey (candidate) @@ -756,9 +770,18 @@ personal names of the form \"family, given\"." (delete-dups (append (citar--fields-in-formats) (when citar-file-variable (list citar-file-variable)) + (when citar-crossref-variable + (list citar-crossref-variable)) citar-additional-fields))) -(defun citar-has-file () +(defun citar-has-file-field (entries) + "Return predicate to test if bibliography entry has a file field." + (when-let ((fieldname citar-file-variable)) + (lambda (key) + (when-let ((entry (map-elt entries key))) + (citar--get-value fieldname entry))))) + +(defun citar-has-file-p (key &optional entry) "Return predicate testing whether entry has associated files. Return a function that takes arguments KEY and ENTRY and returns @@ -769,11 +792,11 @@ non-nil when the entry has associated files, either in Note: for performance reasons, this function should be called once per command; the function it returns can be called repeatedly." - (citar-file--has-file citar-library-paths - citar-library-file-extensions - citar-file-variable)) + (when-let ((entry (or entry (citar--get-entry entry))) + (hasfilep (citar-has-files-for-entries '((key . entry))))) + (funcall hasfilep key))) -(defun citar-has-note () +(defun citar-has-note-p (key &optional entry) "Return predicate testing whether entry has associated notes. Return a function that takes arguments KEY and ENTRY and returns @@ -782,13 +805,45 @@ 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." - ;; Call each function in `citar-has-note-functions` to get a list of predicates - (let ((preds (mapcar #'funcall citar-has-note-functions))) - ;; Return a predicate that checks if `citekey` and `entry` have a note - (lambda (citekey &optional entry) - (let ((nentry (or entry (citar--get-entry citekey)))) - ;; Call each predicate with `citekey` and `entry`; return the first non-nil result - (seq-some (lambda (pred) (funcall pred citekey nentry)) preds))))) + (when-let ((entry (or entry (citar--get-entry entry))) + (hasnotep (citar-has-notes-for-entries '((key . entry))))) + (funcall hasnotep key))) + +(defun citar-has-notes-for-entries (entries) + (citar--has-resources-for-entries citar-has-note-functions entries)) + +(defun citar-has-files-for-entries (entries) + (citar--has-resources-for-entries citar-has-file-functions entries)) + +(defun citar--has-resources-for-entries (functions entries) + "Return predicate combining results of calling FUNCTIONS. + +FUNCTIONS should be a list of functions, each of which returns a +predicate function that takes KEY and ENTRY arguments. Run each +function in the list, and return a predicate that is the logical +or of all these predicates. + +The FUNCTIONS may also return nil, which is treated as an +always-false predicate and ignored. If there is only one non-nil +predicate, return it." + (when-let ((predicates (delq nil (mapcar (lambda (fn) + (funcall fn entries)) + functions)))) + (let ((hasresourcep (if (null (cdr predicates)) + ;; optimization for single predicate; just use it directly + (car predicates) + ;; otherwise, call all predicates until one returns non-nil + (lambda (citekey) + (seq-some (lambda (predicate) + (funcall predicate citekey)) + predicates))))) + (if-let ((crossref citar-crossref-variable)) + (lambda (citekey) + (or (funcall hasresourcep citekey) + (when-let ((entry (map-elt entries citekey)) + (crossrefkey (citar--get-value crossref entry))) + (funcall hasresourcep crossrefkey)))) + hasresourcep)))) (defun citar--ref-affix (cands) "Add affixation prefix to CANDS." From 3ae93f9e5590ffc06a22a68942c1b120d5e8de12 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 14:46:48 -0400 Subject: [PATCH 31/78] Move embark code to separate package Doesn't work ATM though. --- citar-embark.el | 124 ++++++++++++++++++++++++++++++++++++++++++++++++ citar.el | 40 ---------------- 2 files changed, 124 insertions(+), 40 deletions(-) create mode 100644 citar-embark.el diff --git a/citar-embark.el b/citar-embark.el new file mode 100644 index 00000000..e26730a7 --- /dev/null +++ b/citar-embark.el @@ -0,0 +1,124 @@ +;;; citar-embark.el --- Citar/Embark integration -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Bruce D'Arcus +;; +;; Author: Bruce D'Arcus +;; Maintainer: Bruce D'Arcus +;; Created: June 22, 2022 +;; Modified: June 22, 2022 +;; Version: 1.0 +;; Keywords: bib extensions +;; Homepage: https://github.com/emacs-citar/citar-embark +;; Package-Requires: ((emacs "27.2") (embark "0.17") (citar "0.9.5")) +;; +;; This file is not part of GNU Emacs. +;; +;;; Commentary: +;; +;; Description +;; +;;; Code: + +(require 'citar) +(require 'embark) + +(declare-function 'citar--key-finder "citar") +;;; variables + +(defvar citar-embark-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "c") #'citar-insert-citation) + (define-key map (kbd "k") #'citar-insert-keys) + (define-key map (kbd "r") #'citar-copy-reference) + (define-key map (kbd "R") #'citar-insert-reference) + (define-key map (kbd "b") #'citar-insert-bibtex) + (define-key map (kbd "o") #'citar-open) + (define-key map (kbd "e") #'citar-open-entry) + (define-key map (kbd "l") #'citar-open-link) + (define-key map (kbd "n") #'citar-open-notes) + (define-key map (kbd "f") #'citar-open-library-file) + (define-key map (kbd "RET") #'citar-run-default-action) + map) + "Keymap for Embark minibuffer actions.") + +(defvar citar-embark-citation-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "i") #'citar-insert-edit) + (define-key map (kbd "o") #'citar-open) + (define-key map (kbd "e") #'citar-open-entry) + (define-key map (kbd "l") #'citar-open-link) + (define-key map (kbd "n") #'citar-open-notes) + (define-key map (kbd "f") #'citar-open-library-file) + (define-key map (kbd "r") #'citar-copy-reference) + (define-key map (kbd "RET") #'citar-run-default-action) + map) + "Keymap for Embark citation-key actions.") + +;;; functions + +(defalias 'citar-embark-key-finder 'citar--key-finder + "Return key-at-point target for Embark.") + +(defun citar-embark--reference-transformer (type target) + "Look up key for a citar-reference TYPE and TARGET." + ;; REVIEW with key-based API, this likely needs adjustment + (cons type (or (gethash target + (with-current-buffer (embark--target-buffer) + (citar--get-candidates)))))) + +(defun citar-embark--selected () + "Return selected candidates from `citar--select-multiple' for embark." + ;; REVIEW does this work? + (when-let (((eq minibuffer-history-variable 'citar-history)) + (metadata (embark--metadata)) + (group-function (completion-metadata-get metadata 'group-function)) + (cands (all-completions + "" minibuffer-completion-table + (lambda (cand) + (and (equal "Selected" (funcall group-function cand nil)) + (or (not minibuffer-completion-predicate) + (funcall minibuffer-completion-predicate cand))))))) + (cons (completion-metadata-get metadata 'category) cands))) + +;;; minor mode + +(defun citar-embark-setup () + "Setup 'citar-embark-mode'." + (set-keymap-parent citar-embark-map embark-general-map) + (dolist (target-finder '(citar-citation-finder citar-embark-key-finder)) + (add-to-list 'embark-target-finders target-finder)) + + (add-to-list 'embark-transformer-alist + '(citar-reference . citar-embark--reference-transformer)) + (add-to-list 'embark-candidate-collectors #'citar-embark--selected) + + (dolist (keymap-cons + '((citar-reference . citar-embark-map) + (citar-key . citar-embark-citation-map) + (citar-citation . citar-embark-citation-map))) + (add-to-list 'embark-keymap-alist keymap-cons)) + + (add-to-list 'embark-target-injection-hooks + '(citar-insert-edit embark--ignore-target)) + + (dolist (command (list #'citar-insert-bibtex #'citar-insert-citation + #'citar-insert-reference #'citar-copy-reference + #'citar-insert-keys #'citar-run-default-action)) + (add-to-list 'embark-multitarget-actions command))) + +(defun citar-embark-reset () + "Reset 'citar-embark-mode' to default." + ;; TODO + (delete 'citar-embark-key-finder embark-target-finders)) + +;;;###autoload +(define-minor-mode citar-embark-mode + "Toggle citar-embark-mode." + :global t + :group 'citar + :lighter " citar-embark" + (if citar-embark-mode (citar-embark-setup) + (citar-embark-reset))) + +(provide 'citar-embark) +;;; citar-embark.el ends here diff --git a/citar.el b/citar.el index 21b47743..8f604d20 100644 --- a/citar.el +++ b/citar.el @@ -940,46 +940,6 @@ predicate, return it." "Return a list of KEYS as a crm-string for `embark'." (if (listp keys) (string-join keys " & ") keys)) -(defun citar--reference-transformer (type target) - "Look up key for a citar-reference TYPE and TARGET." - (cons type (citar--extract-candidate-citekey target))) - -(defun citar--embark-selected () - "Return selected candidates from `citar--select-multiple' for embark." - (when-let (((eq minibuffer-history-variable 'citar-history)) - (metadata (embark--metadata)) - (group-function (completion-metadata-get metadata 'group-function)) - (cands (all-completions - "" minibuffer-completion-table - (lambda (cand) - (and (equal "Selected" (funcall group-function cand nil)) - (or (not minibuffer-completion-predicate) - (funcall minibuffer-completion-predicate cand))))))) - (cons (completion-metadata-get metadata 'category) cands))) - -;;;###autoload -(with-eval-after-load 'embark - (add-to-list 'embark-target-finders 'citar-citation-finder) - (add-to-list 'embark-transformer-alist - '(citar-reference . citar--reference-transformer)) - (add-to-list 'embark-target-finders 'citar-key-finder) - (add-to-list 'embark-candidate-collectors #'citar--embark-selected)) - -(with-eval-after-load 'embark - (set-keymap-parent citar-map embark-general-map) - (add-to-list 'embark-keymap-alist '(citar-reference . citar-map)) - (add-to-list 'embark-keymap-alist '(citar-key . citar-citation-map)) - (add-to-list 'embark-keymap-alist '(citar-citation . citar-citation-map)) - (add-to-list (if (boundp 'embark-allow-edit-actions) - 'embark-pre-action-hooks - 'embark-target-injection-hooks) - '(citar-insert-edit embark--ignore-target)) - (when (boundp 'embark-multitarget-actions) - (dolist (command (list #'citar-insert-bibtex #'citar-insert-citation - #'citar-insert-reference #'citar-copy-reference - #'citar-insert-keys #'citar-run-default-action)) - (add-to-list 'embark-multitarget-actions command)))) - ;;; Commands ;;;###autoload From c62e0cbbc418fdfa86c4becc32fba77cfb35192b Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 15:04:53 -0400 Subject: [PATCH 32/78] Make get-entry and get-value public (again) --- CONTRIBUTING.org | 12 ++++++------ citar-file.el | 10 +++++----- citar.el | 31 +++++++++++++++---------------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org index 14bfb9d4..d746c986 100644 --- a/CONTRIBUTING.org +++ b/CONTRIBUTING.org @@ -16,16 +16,16 @@ Citar uses a cache, which stores two hash tables for each bibliography file: - entries :: as returned by =parsebib-parse=, keys are citekeys, values are alists of entry fields - pre-formatted :: values are partially-formatted completion strings -The =citar--ref-completion-table= function returns a hash table from the bibliographic cache, and ~citar--get-entry~ and ~-citar--get-value~ provide access to those data. +The =citar--ref-completion-table= function returns a hash table from the bibliographic cache, and ~citar-get-entry~ and ~-citar-get-value~ provide access to those data. Most user-accessible citar functions take an argument ~key~ or ~keys~. -Some functions also take an ~entry~ argument, and ~citar--get-value~ takes either. -When using these functions, you should keep in mind that unless you pass an entry alist to ~citar--get-value~, and instead use a key, each call to that function will query the cache. +Some functions also take an ~entry~ argument, and ~citar-get-value~ takes either. +When using these functions, you should keep in mind that unless you pass an entry alist to ~citar-get-value~, and instead use a key, each call to that function will query the cache. This, therefore, is a better pattern to use: #+begin_src emacs-lisp -(let* ((entry (citar--get-entry key)) - (title (citar--get-value entry "title"))) +(let* ((entry (citar-get-entry key)) + (title (citar-get-value entry "title"))) (message title)) #+end_src @@ -45,7 +45,7 @@ An example: (let* ((files (seq-mapcat (lambda (key) (citar-file--files-for-entry - key (citar--get-entry key) + key (citar-get-entry key) '("/") '("pdf"))) keys )) (output (seq-map diff --git a/citar-file.el b/citar-file.el index 97fbb240..a4e2eee2 100644 --- a/citar-file.el +++ b/citar-file.el @@ -38,8 +38,8 @@ (make-obsolete-variable 'citar-file-extensions 'citar-library-file-extensions "1.0") -(declare-function citar--get-entry "citar") -(declare-function citar--get-value "citar") +(declare-function citar-get-entry "citar") +(declare-function citar-get-value "citar") (declare-function citar--get-template "citar") ;;;; File related variables @@ -142,7 +142,7 @@ File names are expanded relative to the elements of DIRS. Filter by EXTENSIONS when present." (unless dirs (setq dirs (list "/"))) ; make sure DIRS is non-nil - (let* ((filefield (citar--get-value fieldname entry)) + (let* ((filefield (citar-get-value fieldname entry)) (files (when filefield (delete-dups @@ -164,8 +164,8 @@ it in matched file names. The returned regexp captures the key as group 1, the extension as group 2, and any additional text following the key as group 3." (let* ((entry (when keys - (citar--get-entry (car keys)))) - (xref (citar--get-value "crossref" entry))) + (citar-get-entry (car keys)))) + (xref (citar-get-value "crossref" entry))) (unless (or (null xref) (string-empty-p xref)) (push xref keys)) (when (and (null keys) (string-empty-p additional-sep)) diff --git a/citar.el b/citar.el index 8f604d20..5bb0b3e8 100644 --- a/citar.el +++ b/citar.el @@ -54,7 +54,6 @@ ;; make all these private (make-obsolete 'citar-get-template 'citar--get-template "1.0") (make-obsolete 'citar-get-link 'citar--get-link "1.0") -(make-obsolete 'citar-get-value 'citar--get-value "1.0") (make-obsolete 'citar-display-value 'citar--display-value "1.0") (make-obsolete 'citar-open-multi 'citar--open-multi "1.0") (make-obsolete 'citar-select-group-related-resources @@ -495,7 +494,7 @@ and other completion functions." (when (or filter predicate) (lambda (cand _) (let* ((key (gethash cand candidates)) - (entry (citar--get-entry key))) + (entry (citar-get-entry key))) (and (or (null filter) (funcall filter key entry)) (or (null predicate) (funcall predicate string)))))))) (complete-with-action action candidates string predicate)))))) @@ -700,23 +699,23 @@ If no function is found, the DEFAULT function is called." ;;; Data access functions -(defun citar--get-entry (key) +(defun citar-get-entry (key) "Return entry for KEY, as an association list." (citar-cache--entry key (citar--bibliographies))) (defun citar--get-entries () (citar-cache--entries (citar--bibliographies))) -(defun citar--get-value (field key-or-entry) +(defun citar-get-value (field key-or-entry) "Return FIELD value for KEY-OR-ENTRY." (let ((entry (if (stringp key-or-entry) - (citar--get-entry key-or-entry) + (citar-get-entry key-or-entry) key-or-entry))) (cdr (assoc-string field entry)))) (defun citar--field-with-value (fields entry) "Return the first field that has a value in ENTRY among FIELDS ." - (seq-find (lambda (field) (citar--get-value field entry)) fields)) + (seq-find (lambda (field) (citar-get-value field entry)) fields)) (defun citar--display-value (fields entry) "Return the first non nil value for ENTRY among FIELDS . @@ -730,7 +729,7 @@ The value is transformed using `citar-display-transform-functions'" string)) citar-display-transform-functions ;; Make sure we always return a string, even if empty. - (or (citar--get-value field entry) "")))) + (or (citar-get-value field entry) "")))) ;; Lifted from bibtex-completion (defun citar-clean-string (s) @@ -778,7 +777,7 @@ personal names of the form \"family, given\"." (when-let ((fieldname citar-file-variable)) (lambda (key) (when-let ((entry (map-elt entries key))) - (citar--get-value fieldname entry))))) + (citar-get-value fieldname entry))))) (defun citar-has-file-p (key &optional entry) "Return predicate testing whether entry has associated files. @@ -791,7 +790,7 @@ non-nil when the entry has associated files, either in Note: for performance reasons, this function should be called once per command; the function it returns can be called repeatedly." - (when-let ((entry (or entry (citar--get-entry entry))) + (when-let ((entry (or entry (citar-get-entry entry))) (hasfilep (citar-has-files-for-entries '((key . entry))))) (funcall hasfilep key))) @@ -804,7 +803,7 @@ 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." - (when-let ((entry (or entry (citar--get-entry entry))) + (when-let ((entry (or entry (citar-get-entry entry))) (hasnotep (citar-has-notes-for-entries '((key . entry))))) (funcall hasnotep key))) @@ -840,7 +839,7 @@ predicate, return it." (lambda (citekey) (or (funcall hasresourcep citekey) (when-let ((entry (map-elt entries citekey)) - (crossrefkey (citar--get-value crossref entry))) + (crossrefkey (citar-get-value crossref entry))) (funcall hasresourcep crossrefkey)))) hasresourcep)))) @@ -902,7 +901,7 @@ predicate, return it." ('pmid "https://www.ncbi.nlm.nih.gov/pubmed/") ('pmcid "https://www.ncbi.nlm.nih.gov/pmc/articles/")))) (when field - (concat base-url (citar--get-value field entry))))) + (concat base-url (citar-get-value field entry))))) ;; REVIEW I removed 'citar--ensure-entries' @@ -991,7 +990,7 @@ For use with `embark-act-all'." (let* ((fn (pcase action ('open 'citar-file-open) ('attach 'mml-attach-file))) - (entry (citar--get-entry key)) + (entry (citar-get-entry key)) (files (citar-file--files-for-entry key @@ -1026,7 +1025,7 @@ With prefix, rebuild the cache before offering candidates." ;; REVIEW KEY, or KEYS (interactive (list (citar-select-ref))) (let* ((embark-default-action-overrides '((file . find-file))) - (entry (citar--get-entry key))) + (entry (citar-get-entry key))) (if (listp citar-open-note-functions) (citar--open-notes (car key) entry) (error "Please change the value of 'citar-open-note-functions' to a list")))) @@ -1092,7 +1091,7 @@ directory as current buffer." With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref :rebuild-cache current-prefix-arg))) - (let* ((entry (citar--get-entry key)) + (let* ((entry (citar-get-entry key)) (link (citar--get-link entry))) (if link (browse-url link) @@ -1148,7 +1147,7 @@ ARG is forwarded to the mode-specific insertion function given in (defun citar-format-reference (keys) "Return formatted reference(s) for the elements of KEYS." - (let* ((entries (mapcar #'citar--get-entry keys)) + (let* ((entries (mapcar #'citar-get-entry keys)) (template (citar--get-template 'preview))) (with-temp-buffer (dolist (entry entries) From 82dd0ca14c32dcbe31b756dbdecfd6e3f03f8c09 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 15:12:34 -0400 Subject: [PATCH 33/78] Remove embark declarations --- citar-embark.el | 5 +---- citar.el | 15 --------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/citar-embark.el b/citar-embark.el index e26730a7..1bd6710e 100644 --- a/citar-embark.el +++ b/citar-embark.el @@ -61,10 +61,7 @@ (defun citar-embark--reference-transformer (type target) "Look up key for a citar-reference TYPE and TARGET." - ;; REVIEW with key-based API, this likely needs adjustment - (cons type (or (gethash target - (with-current-buffer (embark--target-buffer) - (citar--get-candidates)))))) + (cons type target)) (defun citar-embark--selected () "Return selected candidates from `citar--select-multiple' for embark." diff --git a/citar.el b/citar.el index 5bb0b3e8..79635aaf 100644 --- a/citar.el +++ b/citar.el @@ -67,21 +67,6 @@ (make-obsolete-variable 'citar-format-note-function 'citar-create-note-function "1.0") -;;; Declare variables and functions for byte compiler - -(defvar embark-keymap-alist) -(defvar embark-target-finders) -(defvar embark-pre-action-hooks) -(defvar embark-general-map) -(defvar embark-meta-map) -(defvar embark-transformer-alist) -(defvar embark-multitarget-actions) -(defvar embark-default-action-overrides) -(defvar embark-candidate-collectors) - -(declare-function embark--target-buffer "ext:embark") -(declare-function embark--metadata "ext:embark") - ;;; Variables (defgroup citar nil From 0e618ca2276feca9ff57327dd86aff96c07ef63e Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 16:37:21 -0400 Subject: [PATCH 34/78] declare embark variable --- citar-embark.el | 1 - citar.el | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/citar-embark.el b/citar-embark.el index 1bd6710e..6300b3ab 100644 --- a/citar-embark.el +++ b/citar-embark.el @@ -22,7 +22,6 @@ (require 'citar) (require 'embark) -(declare-function 'citar--key-finder "citar") ;;; variables (defvar citar-embark-map diff --git a/citar.el b/citar.el index 79635aaf..0dc095c0 100644 --- a/citar.el +++ b/citar.el @@ -392,6 +392,9 @@ When nil, all citar commands will use `completing-read`." :type 'boolean :group 'citar) +(defvar 'embark-default-action-overrides) + + ;;; Keymaps (defvar citar-map From 2a22fcc1a6d771be96245d0a76b7982a23f7078b Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Tue, 21 Jun 2022 10:09:35 -0600 Subject: [PATCH 35/78] Move Embark integration to `citar-embark.el` --- citar-embark.el | 155 ++++++++++++++++++++++++++++++++++++++++++++++++ citar.el | 70 +--------------------- 2 files changed, 157 insertions(+), 68 deletions(-) create mode 100644 citar-embark.el diff --git a/citar-embark.el b/citar-embark.el new file mode 100644 index 00000000..50af6eef --- /dev/null +++ b/citar-embark.el @@ -0,0 +1,155 @@ +;;; citar-embark.el --- Integrate citar with embark -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2021 Bruce D'Arcus +;; +;; This file is not part of GNU Emacs. +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . +;; +;;; Commentary: +;; +;; Add embark functionality to citar. +;; +;;; Code: + +(require 'citar) +(require 'embark) + +;;;; Keymaps + +(defvar citar-embark-map (make-composed-keymap citar-map embark-general-map) + "Keymap for Embark actions on Citar references.") + +;; TODO should this also inherit from `embark-general-map'? +(defvar citar-embark-citation-map (make-composed-keymap citar-citation-map nil) + "Keymap for Embark actions on Citar citations and keys.") + +;;; At-point functions for Embark + +(defun citar-embark--key-finder () + "Return the citation key at point." + (when-let (key (and (not (minibufferp)) + (citar--major-mode-function 'key-at-point #'ignore))) + (cons 'citar-key key))) + +(defun citar-embark--citation-finder () + "Return the keys of the citation at point." + (when-let (citation (and (not (minibufferp)) + (citar--major-mode-function 'citation-at-point #'ignore))) + `(citar-citation ,(citar--stringify-keys (car citation)) . ,(cdr citation)))) + +(defun citar-embark--candidate-transformer (_type target) + "Look up key for a citar-reference TYPE and TARGET." + (cons 'citar-reference (citar--extract-candidate-citekey target))) + +(defun citar-embark--selected () + "Return selected candidates from `citar--select-multiple' for embark." + (when-let (((eq minibuffer-history-variable 'citar-history)) + (metadata (embark--metadata)) + (group-function (completion-metadata-get metadata 'group-function)) + (cands (all-completions + "" minibuffer-completion-table + (lambda (cand) + (and (equal "Selected" (funcall group-function cand nil)) + (or (not minibuffer-completion-predicate) + (funcall minibuffer-completion-predicate cand))))))) + (cons (completion-metadata-get metadata 'category) cands))) + +;;;###autoload +(define-minor-mode citar-embark-mode + "Toggle Citar target finders for Embark." + :group 'citar + :global t + :init-value nil + :lighter nil + (let ((targetfinders (list #'citar-embark--key-finder #'citar-embark--citation-finder)) + (collectors (list #'citar-embark--selected)) + (transformers (list (cons 'citar-candidate #'citar-embark--candidate-transformer))) + (keymaps '((citar-reference . citar-embark-map) ; minibuffer candidates + (citar-key . citar-embark-citation-map) ; at-point keys + (citar-citation . citar-embark-citation-map))) + (multitarget (list #'citar-insert-bibtex #'citar-insert-citation + #'citar-insert-reference #'citar-copy-reference + #'citar-insert-keys #'citar-run-default-action)) + (ignoretarget (list #'citar-insert-edit))) ; at-point citations + (if citar-embark-mode + (progn + ;; Add target finders for `embark-act' + (dolist (targetfinder (reverse targetfinders)) + (add-hook 'embark-target-finders targetfinder)) + + ;; Add collectors for `embark-collect', `embark-act-all', etc. + (dolist (collector (reverse collectors)) + (add-hook 'embark-candidate-collectors collector)) + + ;; Add target transformers + (dolist (transformer transformers) + (setf (alist-get (car transformer) embark-transformer-alist) (cdr transformer))) + + ;; Add Embark keymaps + (dolist (keymap keymaps) + (setf (alist-get (car keymap) embark-keymap-alist) (cdr keymap))) + + ;; Mark commands as multitarget actions + (dolist (command multitarget) + (cl-pushnew command embark-multitarget-actions)) + + ;; Mark commands as ignoring target + (dolist (command ignoretarget) + (cl-pushnew #'embark--ignore-target + (alist-get command (if (boundp 'embark-setup-action-hooks) + ;; TODO Remove backward compatibility for Embark < 0.15? + embark-setup-action-hooks + embark-target-injection-hooks))))) + ;; Disable citar-embark-mode: + + ;; Remove target finders + (dolist (targetfinder targetfinders) + (remove-hook 'embark-target-finders targetfinder)) + + ;; Remove target collectors + (dolist (collector collectors) + (remove-hook 'embark-candidate-collectors collector)) + + ;; Remove target transformers + (dolist (transformer transformers) + (cl-callf2 assq-delete-all (car transformer) embark-transformer-alist)) + + ;; Remove Embark keymaps + (dolist (keymap keymaps) + (cl-callf2 assq-delete-all (car keymap) embark-transformer-alist)) + + ;; Remove commands from embark-multitarget-actions + (cl-callf cl-set-difference embark-multitarget-actions multitarget) + + ;; Remove #'embark--ignore-target setup hook + (dolist (command ignoretarget) + ;; TODO simplfy this when we drop compatibility with Embark < 0.15 + (cl-callf (lambda (hookalist) + (when-let ((alistentry (assq command hookalist))) + (cl-callf2 remq #'embark--ignore-target (cdr alistentry)) + (unless (cdr alistentry) ; if no other hooks, remove alist entry + (cl-callf2 remq alistentry hookalist))) + hookalist) + (if (boundp 'embark-setup-action-hooks) + embark-setup-action-hooks + embark-target-injection-hooks)))))) + +;;;###autoload +(with-eval-after-load 'citar + (with-eval-after-load 'embark + (citar-embark-mode))) + +(provide 'citar-embark) +;;; citar-embark.el ends here diff --git a/citar.el b/citar.el index a0aaabc6..2dcaee8c 100644 --- a/citar.el +++ b/citar.el @@ -71,18 +71,7 @@ ;;; Declare variables and functions for byte compiler -(defvar embark-keymap-alist) -(defvar embark-target-finders) -(defvar embark-pre-action-hooks) -(defvar embark-general-map) -(defvar embark-meta-map) -(defvar embark-transformer-alist) -(defvar embark-multitarget-actions) (defvar embark-default-action-overrides) -(defvar embark-candidate-collectors) - -(declare-function embark--target-buffer "ext:embark") -(declare-function embark--metadata "ext:embark") ;;; Variables @@ -485,7 +474,7 @@ the form (KEY . VAL). The returned completion table can be used with `completing-read` and other completion functions." - (let ((metadata `(metadata . ((category . citar-reference) + (let ((metadata `(metadata . ((category . citar-candidate) . ((affixation-function . ,#'citar--ref-affix) . ,metadata))))) (lambda (string predicate action) @@ -657,6 +646,7 @@ Return nil if there are no bibliography files or no entries." (lambda (citekey _entry) (let* ((hasfile (and hasfilep (funcall hasfilep citekey))) (hasnote (and hasnotep (funcall hasnotep citekey))) + (hasnote t) (preform (or (gethash citekey preformatted) (error "No preformatted candidate string: %s" citekey))) (display (citar-format--star-widths @@ -917,66 +907,10 @@ predicate, return it." (search (completing-read "Preset: " citar-presets))) (insert search))) -;;; At-point functions for Embark - -;;;###autoload -(defun citar-key-finder () - "Return the citation key at point." - (when-let (key (and (not (minibufferp)) - (citar--major-mode-function 'key-at-point #'ignore))) - (cons 'citar-key key))) - -;;;###autoload -(defun citar-citation-finder () - "Return the keys of the citation at point." - (when-let (citation (and (not (minibufferp)) - (citar--major-mode-function 'citation-at-point #'ignore))) - `(citar-citation ,(citar--stringify-keys (car citation)) . ,(cdr citation)))) - (defun citar--stringify-keys (keys) "Return a list of KEYS as a crm-string for `embark'." (if (listp keys) (string-join keys " & ") keys)) -(defun citar--reference-transformer (type target) - "Look up key for a citar-reference TYPE and TARGET." - (cons type (citar--extract-candidate-citekey target))) - -(defun citar--embark-selected () - "Return selected candidates from `citar--select-multiple' for embark." - (when-let (((eq minibuffer-history-variable 'citar-history)) - (metadata (embark--metadata)) - (group-function (completion-metadata-get metadata 'group-function)) - (cands (all-completions - "" minibuffer-completion-table - (lambda (cand) - (and (equal "Selected" (funcall group-function cand nil)) - (or (not minibuffer-completion-predicate) - (funcall minibuffer-completion-predicate cand))))))) - (cons (completion-metadata-get metadata 'category) cands))) - -;;;###autoload -(with-eval-after-load 'embark - (add-to-list 'embark-target-finders 'citar-citation-finder) - (add-to-list 'embark-transformer-alist - '(citar-reference . citar--reference-transformer)) - (add-to-list 'embark-target-finders 'citar-key-finder) - (add-to-list 'embark-candidate-collectors #'citar--embark-selected)) - -(with-eval-after-load 'embark - (set-keymap-parent citar-map embark-general-map) - (add-to-list 'embark-keymap-alist '(citar-reference . citar-map)) - (add-to-list 'embark-keymap-alist '(citar-key . citar-citation-map)) - (add-to-list 'embark-keymap-alist '(citar-citation . citar-citation-map)) - (add-to-list (if (boundp 'embark-allow-edit-actions) - 'embark-pre-action-hooks - 'embark-target-injection-hooks) - '(citar-insert-edit embark--ignore-target)) - (when (boundp 'embark-multitarget-actions) - (dolist (command (list #'citar-insert-bibtex #'citar-insert-citation - #'citar-insert-reference #'citar-copy-reference - #'citar-insert-keys #'citar-run-default-action)) - (add-to-list 'embark-multitarget-actions command)))) - ;;; Commands ;;;###autoload From bd35c23a3c5e2b501bd22b61496384dde959ebc2 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 17:47:57 -0600 Subject: [PATCH 36/78] Small docstring tweaks; improve file outline headings --- citar-file.el | 14 ++++++++++++-- citar.el | 29 ++++++++++++++++++----------- test/citar-format-test.el | 3 --- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/citar-file.el b/citar-file.el index 97fbb240..a269a12b 100644 --- a/citar-file.el +++ b/citar-file.el @@ -197,15 +197,25 @@ separating the key from the additional text. When KEYS is nil and ADDITIONAL-SEP is non-nil, each file name is stored in the hash table under two keys: the base name of the -file and the portion of the file name preceding the first match +file; and the portion of the file name preceding the first match of ADDITIONAL-SEP. +When KEYS is nil, if ADDITIONAL-SEP is empty then it is treated +as being nil. In other words, this function can only scan a +directory for file names matching unknown keys if either + +1. The key is not followed by any additional text except for the + file extension. + +2. There is a non-empty ADDITIONAL-SEP between the key and any + following text. + Note: when KEYS and EXTENSIONS are non-nil and ADDITIONAL-SEP is nil, this function has an optimized implementation; it checks for existing files named \"KEY.EXT\" in DIRS, with KEY and EXT being the elements of KEYS and EXTENSIONS, respectively. It does not need to scan the contents of DIRS in this case." - (let ((files (make-hash-table :test #'equal)) + (let ((files (make-hash-table :test 'equal)) (filematch (unless (and keys extensions (not additional-sep)) (citar-file--make-filename-regexp keys extensions additional-sep)))) (prog1 files diff --git a/citar.el b/citar.el index 2dcaee8c..d5ea917b 100644 --- a/citar.el +++ b/citar.el @@ -40,7 +40,6 @@ (require 'cl-lib) (require 'subr-x)) (require 'seq) -(require 'map) (require 'browse-url) (require 'citar-cache) (require 'citar-format) @@ -75,6 +74,8 @@ ;;; Variables +;;;; Faces + (defgroup citar nil "Citations and bibliography management." :group 'editing) @@ -94,6 +95,8 @@ "Face used for the currently selected candidates." :group 'citar) +;;;; Bibliography, file, and note paths + (defcustom citar-bibliography nil "A list of bibliography files." :group 'citar @@ -145,6 +148,8 @@ to include." :group 'citar :type '(repeat string)) +;;;; Displaying completions and formatting + (defcustom citar-templates '((main . "${author editor:30} ${date year issued:4} ${title:48}") (suffix . " ${=key= id:15} ${=type=:12} ${tags keywords keywords:*}") @@ -163,14 +168,14 @@ for the title field for new notes." (defcustom citar-ellipsis nil "Ellipsis string to mark ending of truncated display fields. -If t, use the value of `truncate-string-ellipsis`. If nil, no +If t, use the value of `truncate-string-ellipsis'. If nil, no ellipsis will be used. Otherwise, this should be a non-empty string specifying the ellipsis." :group 'citar - :type '(choice (const :tag "Use 'truncate-string-ellipsis'" t) - (const :tag "Disable ellipsis" nil) - (const "...") + :type '(choice (const :tag "Use `truncate-string-ellipsis'" t) + (const :tag "No ellipsis" nil) (const "…") + (const "...") (string :tag "Ellipsis string"))) (defcustom citar-format-reference-function @@ -241,6 +246,8 @@ the same width." :group 'citar :type 'string) +;;;; Citar actions and other miscellany + (defcustom citar-force-refresh-hook nil "Hook run when user forces a (re-) building of the candidates cache. This hook is only called when the user explicitly requests the @@ -272,7 +279,7 @@ If nil, single resources will open without prompting." :group 'citar :type '(boolean)) -;;; Note-handling setup +;;;; File, note, and URL handling (defcustom citar-open-note-functions '(citar-file--open-note) @@ -382,7 +389,7 @@ of all citations in the current buffer." :group 'citar :type 'alist) -;;; History, including future history list. +;;;; History, including future history list. (defvar citar-history nil "Search history for `citar'.") @@ -398,7 +405,7 @@ When nil, all citar commands will use `completing-read`." :type 'boolean :group 'citar) -;;; Keymaps +;;;; Keymaps (defvar citar-map (let ((map (make-sparse-keymap))) @@ -437,8 +444,8 @@ The elements of BUFFERS are either buffers or the symbol 'global. Returns the absolute file names of the bibliographies in all these contexts. -When BUFFERS is empty, return local bibliographies for the -current buffer and global bibliographies." +When BUFFERS is nil, return local bibliographies for the current +buffer and global bibliographies." (citar-file--normalize-paths (mapcan (lambda (buffer) (if (eq buffer 'global) @@ -446,7 +453,7 @@ current buffer and global bibliographies." (list citar-bibliography)) (with-current-buffer buffer (citar--major-mode-function 'local-bib-files #'ignore)))) - (or buffers (list (current-buffer) 'global))))) + (or buffers (list (current-buffer) 'global))))) (defun citar--bibliographies (&rest buffers) "Return bibliographies for BUFFERS." diff --git a/test/citar-format-test.el b/test/citar-format-test.el index ddeba9b9..205dfb57 100644 --- a/test/citar-format-test.el +++ b/test/citar-format-test.el @@ -2,12 +2,9 @@ ;;; Commentary: -;; - ;;; Code: (require 'ert) -(require 'seq) (require 'citar-format) (ert-deftest citar-format-test--star-widths () From 05418ae5bfd5cd9f95f3f7e0271dfe24ee834a83 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 23 Jun 2022 16:50:34 -0400 Subject: [PATCH 37/78] Remove embark maps from main file --- citar-embark.el | 47 ++++++++++++++++++++++++++++++----------------- citar.el | 32 -------------------------------- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/citar-embark.el b/citar-embark.el index 6300b3ab..af03df7e 100644 --- a/citar-embark.el +++ b/citar-embark.el @@ -24,6 +24,20 @@ ;;; variables +(defvar citar-embark-finders + '(citar-citation-finder + citar-embark-key-finder)) + +(defvar citar-embark-keymaps-alist + '((citar-reference . citar-embark-map) + (citar-key . citar-embark-citation-map) + (citar-citation . citar-embark-citation-map))) + +(defvar citar-embark-multi-commands + (list #'citar-insert-bibtex #'citar-insert-citation + #'citar-insert-reference #'citar-copy-reference + #'citar-insert-keys #'citar-run-default-action)) + (defvar citar-embark-map (let ((map (make-sparse-keymap))) (define-key map (kbd "c") #'citar-insert-citation) @@ -81,31 +95,30 @@ (defun citar-embark-setup () "Setup 'citar-embark-mode'." (set-keymap-parent citar-embark-map embark-general-map) - (dolist (target-finder '(citar-citation-finder citar-embark-key-finder)) + (dolist (target-finder citar-embark-finders) (add-to-list 'embark-target-finders target-finder)) - + (dolist (command citar-embark-multi-commands) + (add-to-list 'embark-multitarget-actions command)) + (dolist (keymap-cons citar-embark-keymaps-alist) + (add-to-list 'embark-keymap-alist keymap-cons)) (add-to-list 'embark-transformer-alist '(citar-reference . citar-embark--reference-transformer)) (add-to-list 'embark-candidate-collectors #'citar-embark--selected) - - (dolist (keymap-cons - '((citar-reference . citar-embark-map) - (citar-key . citar-embark-citation-map) - (citar-citation . citar-embark-citation-map))) - (add-to-list 'embark-keymap-alist keymap-cons)) - (add-to-list 'embark-target-injection-hooks - '(citar-insert-edit embark--ignore-target)) - - (dolist (command (list #'citar-insert-bibtex #'citar-insert-citation - #'citar-insert-reference #'citar-copy-reference - #'citar-insert-keys #'citar-run-default-action)) - (add-to-list 'embark-multitarget-actions command))) + '(citar-insert-edit embark--ignore-target))) (defun citar-embark-reset () "Reset 'citar-embark-mode' to default." - ;; TODO - (delete 'citar-embark-key-finder embark-target-finders)) + (dolist (target-finder citar-embark-finders) + (delete target-finder embark-target-finders)) + (dolist (command citar-embark-multi-commands) + (delete command embark-multitarget-actions)) + (dolist (keymap-cons citar-embark-keymaps-alist) + (assq-delete-all (car keymap-cons) embark-keymap-alist)) + (assq-delete-all 'citar-reference embark-transformer-alist) + (delete #'citar-embark--selected embark-candidate-collectors) + (dolist (hook '(citar-insert-edit embark--ignore-target)) + (delete hook embark-target-injection-hooks))) ;;;###autoload (define-minor-mode citar-embark-mode diff --git a/citar.el b/citar.el index 0dc095c0..5cad59df 100644 --- a/citar.el +++ b/citar.el @@ -394,38 +394,6 @@ When nil, all citar commands will use `completing-read`." (defvar 'embark-default-action-overrides) - -;;; Keymaps - -(defvar citar-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "c") #'citar-insert-citation) - (define-key map (kbd "k") #'citar-insert-keys) - (define-key map (kbd "r") #'citar-copy-reference) - (define-key map (kbd "R") #'citar-insert-reference) - (define-key map (kbd "b") #'citar-insert-bibtex) - (define-key map (kbd "o") #'citar-open) - (define-key map (kbd "e") #'citar-open-entry) - (define-key map (kbd "l") #'citar-open-link) - (define-key map (kbd "n") #'citar-open-notes) - (define-key map (kbd "f") #'citar-open-library-file) - (define-key map (kbd "RET") #'citar-run-default-action) - map) - "Keymap for Embark minibuffer actions.") - -(defvar citar-citation-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "i") #'citar-insert-edit) - (define-key map (kbd "o") #'citar-open) - (define-key map (kbd "e") #'citar-open-entry) - (define-key map (kbd "l") #'citar-open-link) - (define-key map (kbd "n") #'citar-open-notes) - (define-key map (kbd "f") #'citar-open-library-file) - (define-key map (kbd "r") #'citar-copy-reference) - (define-key map (kbd "RET") #'citar-run-default-action) - map) - "Keymap for Embark citation-key actions.") - ;;; Bibliography cache (defun citar--bibliography-files (&rest buffers) From 10bfc0e6f8eb4d6b41d3b7a2327e1599ff5ac9f3 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 18:15:39 -0600 Subject: [PATCH 38/78] Update file and note handling * Rewrite file field parsers to hopefully be more robust. Add parser test suite. Fixes #99, fixes #454. * Separate the library file code from the file field parsing code. Implement them in separate functions that are added to `citar-has-files-functions`, `citar-get-files-functions`, and `citar-get-notes-functions` * Move crossref handling out of file parsing and scanning code, and into `citar.el` --- citar-file.el | 237 +++++++++++++++++++---------------- citar.el | 268 ++++++++++++++++++++++++++++------------ test/citar-file-test.el | 46 +++++++ 3 files changed, 367 insertions(+), 184 deletions(-) create mode 100644 test/citar-file-test.el diff --git a/citar-file.el b/citar-file.el index a269a12b..1612caa8 100644 --- a/citar-file.el +++ b/citar-file.el @@ -27,7 +27,6 @@ (require 'cl-lib) (require 'subr-x)) (require 'seq) -(make-obsolete 'citar-format-note-function 'citar-create-note-function "1.0") ;;; pre-1.0 API cleanup @@ -38,9 +37,9 @@ (make-obsolete-variable 'citar-file-extensions 'citar-library-file-extensions "1.0") -(declare-function citar--get-entry "citar") -(declare-function citar--get-value "citar") -(declare-function citar--get-template "citar") +(declare-function citar-get-value "citar") +(declare-function citar--bibliography-files "citar") +(declare-function citar--check-configuration "citar") ;;;; File related variables @@ -66,6 +65,7 @@ :group 'citar :type '(function)) +;; TODO move this to citar.el for consistency with `citar-library-file-extensions'? (defcustom citar-file-note-extensions '("org" "md") "List of file extensions to filter for notes. @@ -104,57 +104,104 @@ separator that does not otherwise occur in citation keys." (if (stringp file-paths) ;; If path is a string, return as a list. (list (file-truename file-paths)) - (delete-dups - (mapcar - (lambda (p) (file-truename p)) file-paths)))) - -(defun citar-file--parser-default (dirs file-field) - "Return a list of files from DIRS and FILE-FIELD." - (let ((files (split-string file-field "[:;]"))) - (delete-dups - (seq-mapcat - (lambda (dir) - (mapcar - (lambda (file) - (expand-file-name file dir)) files)) - dirs)))) - -(defun citar-file--parser-triplet (dirs file-field) + (delete-dups (mapcar #'file-truename file-paths)))) + +;;;; Parsing file fields + +(defun citar-file--parser-default (file-field) + "Split FILE-FIELD by both : and ;." + (mapcan (lambda (sepchar) + (mapcar #'string-trim + (citar-file--split-escaped-string file-field sepchar))) + ";:")) + +(defun citar-file--parser-triplet (file-field) "Return a list of files from DIRS and a FILE-FIELD formatted as a triplet. This is file-field format seen in, for example, Calibre and Mendeley. Example: ':/path/to/test.pdf:PDF'." - (let ((parts (split-string file-field "[,;]" 'omit-nulls))) - (seq-mapcat - (lambda (part) - (let ((fn (car (split-string part ":" 'omit-nulls)))) - (mapcar (apply-partially #'expand-file-name fn) dirs))) - parts))) - -(defun citar-file--extension-p (filename extensions) - "Return non-nil if FILENAME extension is among EXTENSIONS." - (member (file-name-extension filename) extensions)) - -(defun citar-file--parse-file-field (entry fieldname &optional dirs extensions) - "Return files listed in FIELDNAME of ENTRY. -File names are expanded relative to the elements of DIRS. - -Filter by EXTENSIONS when present." - (unless dirs (setq dirs (list "/"))) ; make sure DIRS is non-nil - (let* ((filefield (citar--get-value fieldname entry)) - (files - (when filefield - (delete-dups - (seq-mapcat - (lambda (parser) - (funcall parser dirs filefield)) - citar-file-parser-functions))))) - (if extensions - (seq-filter - (lambda (fn) - (citar-file--extension-p fn extensions)) files) - files))) + (let (filenames) + (dolist (sepchar '(?\; ?,)) ; Mendeley and Zotero use ;, Calibre uses , + (dolist (substring (citar-file--split-escaped-string file-field sepchar)) + (let* ((triplet (citar-file--split-escaped-string substring ?:)) + (len (length triplet))) + (when (>= len 3) + ;; If there are more than three components, we probably split on unescaped : in the filename. + ;; Take all but the first and last components of TRIPLET and join them with : + (let* ((escaped (string-join (butlast (cdr triplet)) ":")) + (filename (replace-regexp-in-string "\\\\\\(.\\)" "\\1" escaped))) + ;; Calibre doesn't escape file names in BIB files, so try both + ;; See https://github.com/kovidgoyal/calibre/blob/master/src/calibre/library/catalogs/bibtex.py + (push filename filenames) + (push escaped filenames)))))) + (nreverse filenames))) + +(defun citar-file--parse-file-field (file-field dirs) + "Return files listed in FILE-FIELD. +Relative file names are expanded from the first directory in DIRS +in which they are found; if they are not found in any directory, +they are omitted. Files with absolute paths are included as-is, +even if they don't exist." + (let (filenames) + (dolist (parser citar-file-parser-functions) + (dolist (filename (funcall parser file-field)) + (if (or (null dirs) (file-name-absolute-p filename)) + (push filename filenames) + (when-let ((filename (seq-some + (lambda (dir) + (let ((filepath (expand-file-name filename dir))) + (when (file-exists-p filepath) + filepath))) + dirs))) + (push filename filenames))))) + (nreverse filenames))) + +(defun citar-file--has-file-field (entries) + "Return predicate to test if bibliography entry in ENTRIES has a file field. +Note: this function is intended to be used in +`citar-has-files-functions'. Use `citar-has-files' to test +whether entries have associated files." + (when-let ((filefield citar-file-variable)) + (lambda (key) + (when-let ((entry (gethash key entries))) + (citar-get-value filefield entry))))) + +(defun citar-file--get-from-file-field (keys entries) + "Return list of FILES for KEYS given in ENTRIES. + +Parse and return files given in the bibliography field named by +`citar-file-variable'. + +Note: this function is intended to be used in +`citar-get-files-functions'. Use `citar-get-files' to get all +files associated with KEYS." + (when-let ((filefield citar-file-variable)) + (citar--check-configuration 'citar-library-paths) + (let ((dirs (append citar-library-paths (mapcar #'file-name-directory (citar--bibliography-files))))) + (mapcan (lambda (citekey) + (when-let ((entry (gethash citekey entries))) + (citar-file--parse-file-field (citar-get-value citar-file-variable entry) dirs))) + keys)))) + +;;;; Scanning library directories + +(defun citar-file--has-library-files (&optional _entries) + "Return predicate testing whether cite key has library files." + (citar--check-configuration 'citar-library-paths) + (let ((files (citar-file--directory-files + citar-library-paths nil citar-library-file-extensions + citar-file-additional-files-separator))) + (lambda (key) + (gethash key files)))) + +(defun citar-file--get-library-files (keys &optional _entries) + "Return list of files for KEYS in ENTRIES." + (citar--check-configuration 'citar-library-paths) + (let ((files (citar-file--directory-files citar-library-paths keys + citar-library-file-extensions + citar-file-additional-files-separator))) + (mapcan (lambda (key) (gethash key files)) keys))) (defun citar-file--make-filename-regexp (keys extensions &optional additional-sep) "Regexp matching file names starting with KEYS and ending with EXTENSIONS. @@ -163,20 +210,15 @@ that separates the key from optional additional text that follows it in matched file names. The returned regexp captures the key as group 1, the extension as group 2, and any additional text following the key as group 3." - (let* ((entry (when keys - (citar--get-entry (car keys)))) - (xref (citar--get-value "crossref" entry))) - (unless (or (null xref) (string-empty-p xref)) - (push xref keys)) - (when (and (null keys) (string-empty-p additional-sep)) - (setq additional-sep nil)) - (concat - "\\`" - (if keys (regexp-opt keys "\\(?1:") "\\(?1:[^z-a]*?\\)") - (when additional-sep (concat "\\(?3:" additional-sep "[^z-a]*\\)?")) - "\\." - (if extensions (regexp-opt extensions "\\(?2:") "\\(?2:[^.]*\\)") - "\\'"))) + (when (and (null keys) (string-empty-p additional-sep)) + (setq additional-sep nil)) + (concat + "\\`" + (if keys (regexp-opt keys "\\(?1:") "\\(?1:[^z-a]*?\\)") + (when additional-sep (concat "\\(?3:" additional-sep "[^z-a]*\\)?")) + "\\." + (if extensions (regexp-opt extensions "\\(?2:") "\\(?2:[^.]*\\)") + "\\'")) (defun citar-file--directory-files (dirs &optional keys extensions additional-sep) "Return files in DIRS starting with KEYS and ending with EXTENSIONS. @@ -224,8 +266,7 @@ need to scan the contents of DIRS in this case." (if filematch ;; Use regexp to scan directory (dolist (file (directory-files dir nil filematch)) - (let ((key (if keys (car keys) - (and (string-match filematch file) (match-string 1 file)))) + (let ((key (and (string-match filematch file) (match-string 1 file))) (filename (expand-file-name file dir)) (basename (file-name-base file))) (push filename (gethash key files)) @@ -242,41 +283,6 @@ need to scan the contents of DIRS in this case." (puthash key (nreverse filelist) files)) files)))) -(defun citar-file--files-for-entry (key entry dirs extensions) - "Find files related to bibliography item KEY with metadata ENTRY. -See `citar-file--files-for-multiple-entries` for details on DIRS, -EXTENSIONS, and how files are found." - (citar-file--files-for-multiple-entries (list (cons key entry)) dirs extensions)) - -(defun citar-file--files-for-multiple-entries (key-entry-alist dirs extensions) - "Find files related to bibliography items in KEYS-ENTRIES. - -KEY-ENTRY-ALIST is a list of (KEY . ENTRY) pairs. Return a list -of files found in two ways: - -- 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. - -- 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 key-entry-alist)) - (files (citar-file--directory-files dirs keys extensions - citar-file-additional-files-separator))) - (delete-dups - (seq-mapcat - (lambda (key-entry) - (append - (gethash (car key-entry) files) - (seq-filter - #'file-exists-p - (citar-file--parse-file-field (cdr key-entry) citar-file-variable dirs extensions)))) - key-entry-alist)))) - ;;;; Opening and creating files functions (defun citar-file-open (file) @@ -298,13 +304,7 @@ of files found in two ways: nil 0 nil file))) -(defun citar-file-has-library-files (&optional _entries) - "Return predicate testing whether cite key has library files." - (let ((files (citar-file--directory-files - citar-library-paths nil citar-library-file-extensions - citar-file-additional-files-separator))) - (lambda (key) - (gethash key files)))) +;;;; Note files (defun citar-file-has-notes (&optional _entries) "Return predicate testing whether cite key has associated notes." @@ -342,5 +342,26 @@ function that will open a new file if the note is not present." (ext (car extensions))) (expand-file-name (concat key "." ext) dir))))) +;;;; Utility functions + +(defun citar-file--split-escaped-string (string sepchar) + "Split STRING into substrings at unescaped occurrences of SEPCHAR. +A character is escaped in STRING if it is preceded by `\\'. The +`\\' character can also escape itself. Return a list of +substrings of STRING delimited by unescaped occurrences of +SEPCHAR." + (let ((skip (format "^\\\\%c" sepchar)) + strings) + (with-temp-buffer + (insert string) + (goto-char (point-min)) + (while (progn (skip-chars-forward skip) (< (point) (point-max))) + (if (= ?\\ (following-char)) + (ignore-error 'end-of-buffer (forward-char 2)) + (push (delete-and-extract-region (point-min) (point)) strings) + (delete-char 1))) + (push (buffer-string) strings)) + (nreverse strings))) + (provide 'citar-file) ;;; citar-file.el ends here diff --git a/citar.el b/citar.el index d5ea917b..8cf7a6f2 100644 --- a/citar.el +++ b/citar.el @@ -102,12 +102,6 @@ :group 'citar :type '(repeat file)) -(defcustom citar-has-file-functions '(citar-has-file-field - citar-file-has-library-files) - "List of functions to test if an entry has associated files." - :group 'citar - :type '(repeat function)) - (defcustom citar-library-paths nil "A list of files paths for related PDFs, etc." :group 'citar @@ -281,6 +275,18 @@ If nil, single resources will open without prompting." ;;;; File, note, and URL handling +(defcustom citar-has-files-functions (list #'citar-file--has-file-field + #'citar-file--has-library-files) + "List of functions to test if an entry has associated files." + :group 'citar + :type '(repeat function)) + +(defcustom citar-get-files-functions (list #'citar-file--get-from-file-field + #'citar-file--get-library-files) + "List of functions to find files associated with entries." + :group 'citar + :type '(repeat function)) + (defcustom citar-open-note-functions '(citar-file--open-note) "List of functions to open a note." @@ -729,6 +735,154 @@ The value is transformed using `citar-display-transform-functions'" citar-display-transform-functions ;; Make sure we always return a string, even if empty. (or (citar--get-value field entry) "")))) +;;;; File, notes, and links + +(cl-defun citar-get-files (key-or-keys &key (entries (citar-get-entries))) + "Return list of files associated with KEY-OR-KEYS in ENTRIES. + +ENTRIES should be a hash table mapping elements of KEYS to +bibliography entries. ENTRIES should also contain any items that +are potentially cross-referenced from elements of KEYS. + +Find files using `citar-get-files-functions'." + (let* ((keys (citar--with-crossref-keys key-or-keys entries)) + (files (mapcan (lambda (fn) (funcall fn keys entries)) citar-get-files-functions))) + (seq-filter (lambda (filename) + (member (file-name-extension filename) citar-library-file-extensions)) + (delete-dups files)))) + + +(cl-defun citar-get-links (key-or-keys &key (entries (citar-get-entries))) + "Return list of links associated with KEY-OR-KEYS in ENTRIES. + +ENTRIES should be a hash table mapping elements of KEYS to +bibliography entries. ENTRIES should also contain any items that +are potentially cross-referenced from elements of KEYS." + (delete-dups + (mapcan + (lambda (key) + (when-let ((entry (gethash key entries))) + (mapcan + (pcase-lambda (`(,fieldname . ,baseurl)) + (when-let ((fieldvalue (citar-get-value fieldname entry))) + (list (concat baseurl fieldvalue)))) + '((doi . "https://doi.org/") + (pmid . "https://www.ncbi.nlm.nih.gov/pubmed/") + (pmcid . "https://www.ncbi.nlm.nih.gov/pmc/articles/") + (url . nil))))) + (citar--with-crossref-keys key-or-keys entries)))) + + +(cl-defun citar-has-files (&key (entries (citar-get-entries))) + "Return predicate testing whether entry has associated files. + +Return a function that takes KEY and returns non-nil when the +corresponding entry in ENTRIES has associated files. ENTRIES +should be a hash table mapping citation keys to entries, as +returned by `citar-get-entries'. The returned predicated may by +nil if no entries have associated files. + +For example, to test whether KEY has associated files: + + (when-let ((hasfilesp (citar-has-files))) + (funcall hasfilesp KEY)) + +When testing many keys, call this function once and use the +returned predicate repeatedly. + +Files are detected using `citar-has-files-functions', which see. +Also check any bibliography entries that are cross-referenced +from the given KEY; see `citar-crossref-variable'. + +Note: All the potentially cross-referenced entries should be +present in ENTRIES. In most cases, ENTRIES should be its default +value (the result of `citar-get-entries') rather than some +smaller subset." + (citar--has-resources-for-entries + entries + (mapcar (lambda (fn) (funcall fn entries)) + citar-has-files-functions))) + + +(cl-defun citar-has-notes (&key (entries (citar-get-entries))) + "Return predicate testing whether entry has associated notes. + +Return a function that takes KEY and returns non-nil when the +corresponding entry in ENTRIES has associated notes. ENTRIES +should be a hash table mapping citation keys to entries, as +returned by `citar-get-entries'. The returned predicate may be +nil if no entries have associated notes. + +For example, to test whether KEY has associated notes: + + (let ((hasnotesp (citar-has-notes))) + (funcall hasnotesp KEY)) + +When testing many keys, call this function once and use the +returned predicate repeatedly. + +Notes are detected using `citar-has-notes-functions', which see. +Also check any bibliography entries that are cross-referenced +from the given KEY; see `citar-crossref-variable'. + +Note: All the potentially cross-referenced entries should be +present in ENTRIES. In most cases, ENTRIES should be its default +value (the result of `citar-get-entries') rather than some +smaller subset." + (citar--has-resources-for-entries + entries + (mapcar (lambda (fn) (funcall fn entries)) + citar-has-notes-functions))) + + +(cl-defun citar-has-links (&key (entries (citar-get-entries))) + "Return predicate testing whether entry has links. + +Return a function that takes KEY and returns non-nil when the +corresponding entry in ENTRIES has associated links. See the +documentation of `citar-has-files` and `citar-has-notes', which +have similar usage." + (citar--has-resources-for-entries + entries + (lambda (key) + (when-let ((entry (gethash key entries))) + (citar-get-field-with-value '(doi pmid pmcid url) entry))))) + + +(defun citar--has-resources-for-entries (entries predicates) + "Return predicate combining results of calling FUNCTIONS. + +PREDICATES should be a list of functions that take a bibliography +KEY and return non-nil if the item has a resource. It may also be +a single such function. + +Return a predicate that returns non-nil for a given KEY when any +of the elements of PREDICATES return non-nil for that KEY. If +PREDICATES is empty or all its elements are nil, then the +returned predicate is nil. + +When `citar-crossref-variable' is the name of a crossref field, +the returned predicate also tests if an entry cross-references +another entry in ENTRIES that has associated resources." + (when-let ((hasresourcep (if (functionp predicates) + predicates + (let ((predicates (remq nil predicates))) + (if (null (cdr predicates)) + ;; optimization for single predicate; just use it directly + (car predicates) + ;; otherwise, call all predicates until one returns non-nil + (lambda (citekey) + (seq-some (lambda (predicate) + (funcall predicate citekey)) + predicates))))))) + (if-let ((xref citar-crossref-variable)) + (lambda (citekey) + (or (funcall hasresourcep citekey) + (when-let ((entry (gethash citekey entries)) + (xkey (citar-get-value xref entry))) + (funcall hasresourcep xkey)))) + hasresourcep))) + ;; Lifted from bibtex-completion (defun citar-clean-string (s) @@ -771,76 +925,28 @@ personal names of the form \"family, given\"." (list citar-crossref-variable)) citar-additional-fields))) -(defun citar-has-file-field (entries) - "Return predicate to test if bibliography entry has a file field." - (when-let ((fieldname citar-file-variable)) - (lambda (key) - (when-let ((entry (map-elt entries key))) - (citar--get-value fieldname entry))))) - -(defun citar-has-file-p (key &optional entry) - "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." - (when-let ((entry (or entry (citar--get-entry entry))) - (hasfilep (citar-has-files-for-entries '((key . entry))))) - (funcall hasfilep key))) - -(defun citar-has-note-p (key &optional entry) - "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." - (when-let ((entry (or entry (citar--get-entry entry))) - (hasnotep (citar-has-notes-for-entries '((key . entry))))) - (funcall hasnotep key))) - -(defun citar-has-notes-for-entries (entries) - (citar--has-resources-for-entries citar-has-note-functions entries)) - -(defun citar-has-files-for-entries (entries) - (citar--has-resources-for-entries citar-has-file-functions entries)) - -(defun citar--has-resources-for-entries (functions entries) - "Return predicate combining results of calling FUNCTIONS. - -FUNCTIONS should be a list of functions, each of which returns a -predicate function that takes KEY and ENTRY arguments. Run each -function in the list, and return a predicate that is the logical -or of all these predicates. - -The FUNCTIONS may also return nil, which is treated as an -always-false predicate and ignored. If there is only one non-nil -predicate, return it." - (when-let ((predicates (delq nil (mapcar (lambda (fn) - (funcall fn entries)) - functions)))) - (let ((hasresourcep (if (null (cdr predicates)) - ;; optimization for single predicate; just use it directly - (car predicates) - ;; otherwise, call all predicates until one returns non-nil - (lambda (citekey) - (seq-some (lambda (predicate) - (funcall predicate citekey)) - predicates))))) - (if-let ((crossref citar-crossref-variable)) - (lambda (citekey) - (or (funcall hasresourcep citekey) - (when-let ((entry (map-elt entries citekey)) - (crossrefkey (citar--get-value crossref entry))) - (funcall hasresourcep crossrefkey)))) - hasresourcep)))) +(defun citar--with-crossref-keys (key-or-keys entries) + "Return KEY-OR-KEYS augmented with cross-referenced items in ENTRIES. + +KEY-OR-KEYS is either a list KEYS or a single key, which is +converted into KEYS. Return a list containing the elements of +KEYS, with each element followed by the corresponding +cross-referenced key in ENTRIES, if any. + +ENTRIES should be a hash table mapping elements of KEYS to +bibliography entries. ENTRIES should also contain any items that +are potentially cross-referenced from elements of KEYS." + (let ((xref citar-crossref-variable) + (keys (if (listp key-or-keys) key-or-keys (list key-or-keys)))) + (if (not xref) + keys + (mapcan (lambda (key) + (cons key (if-let* ((entry (gethash key entries)) + (xkey (citar-get-value xref entry))) + (list xkey)))) + keys)))) + +;;; Affixations and annotations (defun citar--ref-affix (cands) "Add affixation prefix to CANDS." @@ -1215,5 +1321,15 @@ URL." (citar-run-default-action (if (listp keys) keys (list keys))) (user-error "No citation keys found"))) +(defun citar--check-configuration (variable) + "Signal error if VARIABLE has a value of the wrong type. +VARIABLE should be a Citar customization variable." + (pcase variable + ((or 'citar-library-paths 'citar-notes-paths) + (let ((value (symbol-value variable))) + (unless (and (listp value) + (seq-every-p #'stringp value)) + (error "`%S' should be a list of directories: %S" variable `',value)))))) + (provide 'citar) ;;; citar.el ends here diff --git a/test/citar-file-test.el b/test/citar-file-test.el new file mode 100644 index 00000000..a2a7a1a7 --- /dev/null +++ b/test/citar-file-test.el @@ -0,0 +1,46 @@ +;;; citar-file-test.el --- Tests for citar-file.el -*- lexical-binding: t; -*- + +;;; Commentary: + +;;; Code: + +(require 'ert) +(require 'seq) +(require 'citar-file) + +(ert-deftest citar-format-test--parsing () + + ;; Test the default parser, which splits strings by both : and ; + (should (equal '("") (delete-dups (citar-file--parser-default " ")))) + (should (equal '("foo") (delete-dups (citar-file--parser-default "foo")))) + (should (equal '("foo" "bar" "foo;bar") (delete-dups (citar-file--parser-default "foo;bar")))) + (should (equal '("foo" "bar" "foo ; bar") (delete-dups (citar-file--parser-default " foo ; bar ")))) + (should (equal '("foo : bar" "foo" "bar") (delete-dups (citar-file--parser-default " foo : bar ")))) + (should (equal '("foo:bar" "baz" "foo" "bar;baz") (delete-dups (citar-file--parser-default "foo:bar;baz")))) + + ;; Test escaped delimiters + (should (equal '("foo\\;bar") (delete-dups (citar-file--parser-default "foo\\;bar")))) + (should (equal '("foo" "bar\\" "foo;bar\\") (delete-dups (citar-file--parser-default "foo;bar\\")))) + (should (equal '("foo\\;bar" "baz" "foo\\;bar;baz") + (delete-dups (citar-file--parser-default "foo\\;bar;baz")))) + + ;; Test triplet parser + (should (equal '("foo.pdf") (delete-dups (citar-file--parser-triplet ":foo.pdf:PDF")))) + (should (equal '("foo.pdf:PDF,:bar.pdf" "foo.pdf" "bar.pdf") + (delete-dups (citar-file--parser-triplet ":foo.pdf:PDF,:bar.pdf:PDF")))) + ;; Don't trim spaces in triplet parser since file is delimited by : + (should (equal '(" foo.pdf :PDF, : bar.pdf " " foo.pdf " " bar.pdf ") + (delete-dups (citar-file--parser-triplet ": foo.pdf :PDF, : bar.pdf :PDF")))) + + ;; Test escaped delimiters + (should (equal '("title.pdf") + (delete-dups (citar-file--parser-triplet "Title\\: Subtitle:title.pdf:application/pdf")))) + (should (equal '("C:\\title.pdf" "C\\:\\\\title.pdf") + (delete-dups (citar-file--parser-triplet "Title\\: Subtitle:C\\:\\\\title.pdf:PDF")))) + + ;; Calibre doesn't escape any special characters in filenames, so try that + (should (equal '("C:title.pdf" "C:\\title.pdf") + (delete-dups (citar-file--parser-triplet "Title\\: Subtitle:C:\\title.pdf:PDF"))))) + +(provide 'citar-file-test) +;;; citar-file-test.el ends here From 5946064eaeb60b572cf9a71cf5f5b73bee5b38a3 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 21:45:46 -0600 Subject: [PATCH 39/78] Finish implementing new API and modify other modules to match. --- citar-citeproc.el | 7 +- citar-format.el | 20 +-- citar-latex.el | 5 +- citar-markdown.el | 5 +- citar-org.el | 4 +- citar.el | 311 ++++++++++++++++++++-------------------------- 6 files changed, 149 insertions(+), 203 deletions(-) diff --git a/citar-citeproc.el b/citar-citeproc.el index 3a6acab4..c2b1d947 100644 --- a/citar-citeproc.el +++ b/citar-citeproc.el @@ -96,7 +96,7 @@ accepted.") (setq citar-citeproc-csl-style file))) ;;;###autoload -(defun citar-citeproc-format-reference (keys-entries) +(defun citar-citeproc-format-reference (keys) "Return formatted reference(s) for KEYS-ENTRIES via `citeproc-el`. Formatting follows CSL style set in `citar-citeproc-csl-style`. With prefix-argument, select CSL style." @@ -108,10 +108,7 @@ With prefix-argument, select CSL style." (let* ((style (if (string-match-p "/" citar-citeproc-csl-style) citar-citeproc-csl-style (expand-file-name citar-citeproc-csl-style citar-citeproc-csl-styles-dir))) - (keys (citar--extract-keys keys-entries)) - (bibs (flatten-list - (list citar-bibliography - (citar--major-mode-function 'local-bib-files #'ignore)))) + (bibs (citar--bibliography-files)) (proc (citeproc-create style (citeproc-hash-itemgetter-from-any bibs) (citeproc-locale-getter-from-dir citar-citeproc-csl-locales-dir) diff --git a/citar-format.el b/citar-format.el index 5495b929..8679b0b8 100644 --- a/citar-format.el +++ b/citar-format.el @@ -26,7 +26,7 @@ (eval-when-compile (require 'cl-lib)) -(declare-function citar--display-value "citar") +(declare-function citar-get-display-value "citar") ;;; Formatting bibliography entries @@ -59,7 +59,7 @@ (`(,props . ,fieldnames) (let* ((fieldwidth (plist-get props :width)) (textprops (plist-get props :text-properties)) - (value (citar--display-value fieldnames entry)) + (value (citar-get-display-value fieldnames entry)) (display (citar-format--string value :width fieldwidth :text-properties textprops @@ -83,21 +83,8 @@ ;;; Internal implementation functions -(defun citar-format--fieldspec (fieldspec entry hide-elided ellipsis) - "Format FIELDSPEC using information from ENTRY. -See `citar-format--string` for the meaning of HIDE-ELIDED and ELLIPSIS." - (if (stringp fieldspec) - fieldspec - (let* ((fmtprops (car fieldspec)) - (fieldnames (cdr fieldspec)) - (displaystr (citar--display-value fieldnames entry))) - (apply #'citar-format--string displaystr - :hide-elided hide-elided :ellipsis ellipsis - fmtprops)))) - - (cl-defsubst citar-format--string (string - &key width text-properties hide-elided ellipsis) + &key width text-properties hide-elided ellipsis) "Truncate STRING to WIDTH and apply TEXT-PROPERTIES. If HIDE-ELIDED is non-nil, the truncated part of STRING is covered by a display property that makes it invisible, instead of @@ -109,6 +96,7 @@ display instead of the truncated part of the text." (setq string (truncate-string-to-width string width 0 ?\s ellipsis hide-elided))) string) + (defun citar-format--star-widths (alloc strings &optional hide-elided ellipsis) "Concatenate STRINGS and truncate every other element to fit in ALLOC. Use this function along with `citar-format--preformat' to fit a diff --git a/citar-latex.el b/citar-latex.el index 7466c2a6..7c576827 100644 --- a/citar-latex.el +++ b/citar-latex.el @@ -203,11 +203,10 @@ inserted." (skip-chars-forward "^}") (forward-char 1))) ;;;###autoload -(defun citar-latex-insert-edit (&optional arg) +(defun citar-latex-insert-edit (&optional _arg) "Prompt for keys and call `citar-latex-insert-citation. With ARG non-nil, rebuild the cache before offering candidates." - (citar-latex-insert-citation - (citar--extract-keys (citar-select-refs :rebuild-cache arg)))) + (citar-latex-insert-citation (citar-select-refs))) (defun citar-latex--select-command () "Complete a citation command for LaTeX." diff --git a/citar-markdown.el b/citar-markdown.el index 8d706e08..84cd02cf 100644 --- a/citar-markdown.el +++ b/citar-markdown.el @@ -86,11 +86,10 @@ If INVERT-PROMPT is non-nil, invert the meaning of (insert "; " keyconcat)))))) ;;;###autoload -(defun citar-markdown-insert-edit (&optional arg) +(defun citar-markdown-insert-edit (&optional _arg) "Prompt for keys and call `citar-markdown-insert-citation. With ARG non-nil, rebuild the cache before offering candidates." - (citar-markdown-insert-citation - (citar--extract-keys (citar-select-refs :rebuild-cache arg)))) + (citar-markdown-insert-citation (citar-select-refs))) ;;;###autoload (defun citar-markdown-key-at-point () diff --git a/citar-org.el b/citar-org.el index e76979ff..79b18be3 100644 --- a/citar-org.el +++ b/citar-org.el @@ -163,8 +163,8 @@ With PROC list, limit to specific processor(s)." (defun citar-org-select-key (&optional multiple) "Return a list of keys when MULTIPLE, or else a key string." (if multiple - (citar--extract-keys (citar-select-refs)) - (car (citar-select-ref)))) + (citar-select-refs) + (citar-select-ref))) ;;;###autoload (defun citar-org-insert-citation (keys &optional style) diff --git a/citar.el b/citar.el index 8cf7a6f2..85a40b3c 100644 --- a/citar.el +++ b/citar.el @@ -51,10 +51,13 @@ ;; make public ;; (make-obsolete 'citar--get-candidates 'citar-get-candidates "1.0") +;; Renamed in 1.0 +(make-obsolete 'citar-open-library-file #'citar-open-files "1.0") +(make-obsolete 'citar-open-link #'citar-open-links "1.0") +(make-obsolete 'citar-get-link "replaced by `citar-get-links'." "1.0") ; now returns list + ;; make all these private (make-obsolete 'citar-get-template 'citar--get-template "1.0") -(make-obsolete 'citar-get-link 'citar--get-link "1.0") -(make-obsolete 'citar-get-value 'citar--get-value "1.0") (make-obsolete 'citar-display-value 'citar--display-value "1.0") (make-obsolete 'citar-open-multi 'citar--open-multi "1.0") (make-obsolete 'citar-select-group-related-resources @@ -62,7 +65,7 @@ (make-obsolete 'citar-select-resource 'citar--select-resource "1.0") ;; also rename -(make-obsolete 'citar-has-a-value 'citar--field-with-value "1.0") +(make-obsolete 'citar-has-a-value 'citar-field-with-value "1.0") (make-obsolete 'citar--open-note 'citar-file--open-note "1.0") (make-obsolete-variable @@ -133,7 +136,7 @@ by this variable." (string :tag "Field name") (const :tag "Ignore cross-references" nil))) -(defcustom citar-additional-fields '("doi" "url") +(defcustom citar-additional-fields '("doi" "url" "pmcid" "pmid") "A list of fields to add to parsed data. By default, citar filters parsed data based on the fields @@ -200,25 +203,6 @@ All functions that match a particular field are run in order." :type '(alist :key-type (choice (const t) (repeat string)) :value-type function)) -(defcustom citar-prefilter-entries '(nil . t) - "When non-nil pre-filter note and library files commands. -For commands like 'citar-open-notes', this will only show -completion candidates that have such notes. - -The downside is that, if using Embark and you want to use a different -command for the action, you will not be able to remove the -filter. - -The value should be a cons of the form: - -(FILTER . TOGGLE) - -FILTER turns this on by default - -TOGGLE use prefix arg to toggle behavior" - :group 'citar - :type 'cons) - (defcustom citar-symbols `((file . ("F" . " ")) (note . ("N" . " ")) @@ -294,7 +278,7 @@ If nil, single resources will open without prompting." :group 'citar :type '(function)) -(defcustom citar-has-note-functions '(citar-file-has-notes) +(defcustom citar-has-notes-functions '(citar-file-has-notes) "Functions used for displaying note indicators. Such functions must take KEY and return non-nil when the @@ -328,6 +312,8 @@ FILEPATH: the file name." :group 'citar :type 'function) +;; TODO Move this to `citar-org', since it's only used there? +;; Otherwise it seems to overlap with `citar-default-action' (defcustom citar-at-point-function #'citar-dwim "The function to run for `citar-at-point'." :group 'citar @@ -422,9 +408,9 @@ When nil, all citar commands will use `completing-read`." (define-key map (kbd "b") #'citar-insert-bibtex) (define-key map (kbd "o") #'citar-open) (define-key map (kbd "e") #'citar-open-entry) - (define-key map (kbd "l") #'citar-open-link) + (define-key map (kbd "l") #'citar-open-links) (define-key map (kbd "n") #'citar-open-notes) - (define-key map (kbd "f") #'citar-open-library-file) + (define-key map (kbd "f") #'citar-open-files) (define-key map (kbd "RET") #'citar-run-default-action) map) "Keymap for Embark minibuffer actions.") @@ -434,9 +420,9 @@ When nil, all citar commands will use `completing-read`." (define-key map (kbd "i") #'citar-insert-edit) (define-key map (kbd "o") #'citar-open) (define-key map (kbd "e") #'citar-open-entry) - (define-key map (kbd "l") #'citar-open-link) + (define-key map (kbd "l") #'citar-open-links) (define-key map (kbd "n") #'citar-open-notes) - (define-key map (kbd "f") #'citar-open-library-file) + (define-key map (kbd "f") #'citar-open-files) (define-key map (kbd "r") #'citar-copy-reference) (define-key map (kbd "RET") #'citar-run-default-action) map) @@ -498,12 +484,12 @@ and other completion functions." (when (or filter predicate) (lambda (cand _) (let* ((key (gethash cand candidates)) - (entry (citar--get-entry key))) + (entry (citar-get-entry key))) (and (or (null filter) (funcall filter key entry)) (or (null predicate) (funcall predicate string)))))))) (complete-with-action action candidates string predicate)))))) -(cl-defun citar-select-ref (&optional &key multiple filter) +(cl-defun citar-select-refs (&key (multiple t) filter) "Select bibliographic references. A wrapper around `completing-read' that returns (KEY . ENTRY), @@ -524,7 +510,7 @@ FILTER: if non-nil, should be a predicate function taking (citar-select-ref :filter (citar-has-note)) (citar-select-ref :filter (citar-has-file))" - (let* ((candidates (or (citar--ref-completion-table) + (let* ((candidates (or (citar--format-candidates) (user-error "No bibliography set"))) (chosen (if (and multiple citar-select-multiple) (citar--select-multiple "References: " candidates @@ -539,13 +525,13 @@ FILTER: if non-nil, should be a predicate function taking (gethash choice candidates)) chosen)))) -(cl-defun citar-select-refs (&optional &key filter) +(cl-defun citar-select-ref (&key filter) "Select bibliographic references. 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 :multiple t :filter filter)) + (car (citar-select-refs :multiple nil :filter filter))) (defun citar--multiple-completion-table (selected-hash candidates filter) "Return a completion table for multiple selection. @@ -609,15 +595,14 @@ HISTORY is the `completing-read' history argument." (defun citar--select-resource (files &optional links) "Select resource from a list of FILES, and optionally LINKS." (let* ((files (mapcar - (lambda (cand) - (abbreviate-file-name cand)) + (lambda (file) + (propertize (abbreviate-file-name file) 'multi-category `(file . ,file))) files)) - (resources (append files (remq nil links)))) - (dolist (item resources) - (cond ((string-match "http" item 0) - (push (propertize item 'multi-category `(url . ,item)) resources)) - (t - (push (propertize item 'multi-category `(file . ,item)) resources)))) + (links (mapcar + (lambda (link) + (propertize link 'multi-category `(url . ,link))) + links)) + (resources (delete-dups (append files links)))) (completing-read "Select resource: " (lambda (string predicate action) @@ -625,7 +610,7 @@ HISTORY is the `completing-read' history argument." `(metadata (group-function . citar--select-group-related-resources) (category . multi-category)) - (complete-with-action action (delete-dups resources) string predicate)))))) + (complete-with-action action resources string predicate)))))) (defun citar--select-group-related-resources (resource transform) "Group RESOURCE by type or TRANSFORM." @@ -636,11 +621,11 @@ HISTORY is the `completing-read' history argument." resource) (cond ((member extension citar-file-note-extensions) "Notes") - ((string-match "http" resource 0) "Links") + ((string-prefix-p "http" resource 'ignore-case) "Links") (t "Library Files"))))) -(cl-defun citar--ref-completion-table (&optional (bibs (citar--bibliographies)) - (entries (citar-cache--entries bibs))) +(cl-defun citar--format-candidates (&key (bibs (citar--bibliographies)) + (entries (citar-cache--entries bibs))) "Return completion table for cite keys, as a hash table. In this hash table, keys are a strings with author, date, and title of the reference. Values are the cite keys. @@ -648,28 +633,31 @@ Return nil if there are no bibliography files or no entries." ;; Populate bibliography cache. (when bibs (let* ((preformatted (citar-cache--preformatted bibs)) - (hasnotep (citar-has-notes-for-entries entries)) - (hasfilep (citar-has-files-for-entries entries)) - (hasnotetag (propertize " has:notes" 'invisible t)) - (hasfiletag (propertize " has:files" 'invisible t)) + (hasfilesp (citar-has-files :entries entries)) + (hasnotesp (citar-has-notes :entries entries)) + (haslinksp (citar-has-links :entries entries)) + (hasfilestag (propertize " has:files" 'invisible t)) + (hasnotestag (propertize " has:notes" 'invisible t)) + (haslinkstag (propertize " has:links" 'invisible t)) (symbolswidth (string-width (citar--symbols-string t t t))) (width (- (frame-width) symbolswidth 2)) (completions (make-hash-table :test 'equal :size (hash-table-count entries)))) (maphash (lambda (citekey _entry) - (let* ((hasfile (and hasfilep (funcall hasfilep citekey))) - (hasnote (and hasnotep (funcall hasnotep citekey))) - (hasnote t) + (let* ((hasfiles (and hasfilesp (funcall hasfilesp citekey))) + (hasnotes (and hasnotesp (funcall hasnotesp citekey))) + (haslinks (and haslinksp (funcall haslinksp citekey))) (preform (or (gethash citekey preformatted) (error "No preformatted candidate string: %s" citekey))) (display (citar-format--star-widths (- width (car preform)) (cdr preform) t citar-ellipsis)) - (tagged (if (not (or hasfile hasnote)) + (tagged (if (not (or hasfiles hasnotes haslinks)) display (concat display - (when hasnote hasnotetag) - (when hasfile hasfiletag))))) + (when hasfiles hasfilestag) + (when hasnotes hasnotestag) + (when haslinks haslinkstag))))) (puthash tagged citekey completions))) entries) completions))) @@ -690,10 +678,9 @@ return DEFAULT." (alist-get key (cdr (seq-find - (lambda (modefns) - (let ((modes (car modefns))) - (or (eq t modes) - (apply #'derived-mode-p (if (listp modes) modes (list modes)))))) + (pcase-lambda (`(,modes . ,_functions)) + (or (eq t modes) + (apply #'derived-mode-p (if (listp modes) modes (list modes))))) citar-major-mode-functions)) default)) @@ -704,37 +691,56 @@ If no function is found, the DEFAULT function is called." ;;; Data access functions -(defun citar--get-entry (key) - "Return entry for KEY, as an association list." +(defun citar-get-entry (key) + "Return entry for reference KEY, as an association list. +Note: this function accesses the bibliography cache and should +not be used for retreiving a large number of entries. Instead, +prefer `citar--get-entries'." (citar-cache--entry key (citar--bibliographies))) -(defun citar--get-entries () +(defun citar-get-entries () + "Return all entries for currently active bibliographies. +Return a hash table whose keys are citation keys and values are +the corresponding entries." (citar-cache--entries (citar--bibliographies))) -(defun citar--get-value (field key-or-entry) - "Return FIELD value for KEY-OR-ENTRY." +(defun citar-get-value (field key-or-entry) + "Return value of FIELD in reference KEY-OR-ENTRY. +KEY-OR-ENTRY should be either a string key, or an entry alist as +returned by `citar-get-entry'. Return nil if the FIELD is not +present in KEY-OR-ENTRY." (let ((entry (if (stringp key-or-entry) - (citar--get-entry key-or-entry) + (citar-get-entry key-or-entry) key-or-entry))) (cdr (assoc-string field entry)))) -(defun citar--field-with-value (fields entry) - "Return the first field that has a value in ENTRY among FIELDS ." - (seq-find (lambda (field) (citar--get-value field entry)) fields)) +(defun citar-get-field-with-value (fields key-or-entry) + "Find the first field among FIELDS that has a value in KEY-OR-ENTRY. +Return (FIELD . VALUE), where FIELD is the element of FIELDS that +was found to have a value, and VALUE is its value." + (let ((entry (if (stringp key-or-entry) + (citar-get-entry key-or-entry) + key-or-entry))) + (seq-some (lambda (field) + (when-let ((value (citar-get-value field entry))) + (cons field value))) + fields))) -(defun citar--display-value (fields entry) - "Return the first non nil value for ENTRY among FIELDS . +(defun citar-get-display-value (fields key-or-entry) + "Return the first non nil value for KEY-OR-ENTRY among FIELDS . The value is transformed using `citar-display-transform-functions'" - (let ((field (citar--field-with-value fields entry))) + (let ((fieldvalue (citar-get-field-with-value fields key-or-entry))) (seq-reduce (lambda (string fun) (if (or (eq t (car fun)) - (member field (car fun))) + (seq-contains-p (car fun) (car fieldvalue) #'string=)) (funcall (cdr fun) string) string)) citar-display-transform-functions ;; Make sure we always return a string, even if empty. - (or (citar--get-value field entry) "")))) + (or (cdr fieldvalue) "")))) + + ;;;; File, notes, and links (cl-defun citar-get-files (key-or-keys &key (entries (citar-get-entries))) @@ -883,6 +889,7 @@ another entry in ENTRIES that has associated resources." (funcall hasresourcep xkey)))) hasresourcep))) +;;; Format and display field values ;; Lifted from bibtex-completion (defun citar-clean-string (s) @@ -918,12 +925,12 @@ personal names of the form \"family, given\"." (defun citar--fields-to-parse () "Determine the fields to parse from the template." - (delete-dups (append (citar--fields-in-formats) - (when citar-file-variable - (list citar-file-variable)) - (when citar-crossref-variable - (list citar-crossref-variable)) - citar-additional-fields))) + (delete-dups `(,@(citar--fields-in-formats) + ,@(when citar-file-variable + (list citar-file-variable)) + ,@(when citar-crossref-variable + (list citar-crossref-variable)) + . ,citar-additional-fields))) (defun citar--with-crossref-keys (key-or-keys entries) "Return KEY-OR-KEYS augmented with cross-referenced items in ENTRIES. @@ -959,9 +966,9 @@ are potentially cross-referenced from elements of KEYS." (defun citar--ref-make-symbols (cand) "Make CAND annotation or affixation string for has-symbols." (let ((candidate-symbols (citar--symbols-string - (string-match "has:file" cand) - (string-match "has:note" cand) - (string-match "has:link" cand)))) + (string-match-p "has:files" cand) + (string-match-p "has:notes" cand) + (string-match-p "has:links" cand)))) candidate-symbols)) (defun citar--ref-annotate (cand) @@ -998,18 +1005,6 @@ are potentially cross-referenced from elements of KEYS." (propertize (citar--get-template 'suffix) 'face 'citar))) (error "No template for \"%s\" - check variable 'citar-templates'" template-name))) -(defun citar--get-link (entry) - "Return a link for an ENTRY." - (let* ((field (citar--field-with-value '(doi pmid pmcid url) entry)) - (base-url (pcase field - ('doi "https://doi.org/") - ('pmid "https://www.ncbi.nlm.nih.gov/pubmed/") - ('pmcid "https://www.ncbi.nlm.nih.gov/pmc/articles/")))) - (when field - (concat base-url (citar--get-value field entry))))) - -;; REVIEW I removed 'citar--ensure-entries' - ;;;###autoload (defun citar-insert-preset () "Prompt for and insert a predefined search." @@ -1021,8 +1016,12 @@ are potentially cross-referenced from elements of KEYS." (insert search))) (defun citar--stringify-keys (keys) - "Return a list of KEYS as a crm-string for `embark'." - (if (listp keys) (string-join keys " & ") keys)) + "Encode a list of KEYS as a single string." + (combine-and-quote-strings (if (listp keys) keys (list keys)) " & ")) + +(defun citar--unstringify-keys (keystring) + "Split KEYSTRING into a list of keys." + (split-string-and-unquote keystring " & ")) ;;; Commands @@ -1031,25 +1030,13 @@ are potentially cross-referenced from elements of KEYS." "Open related resources (links or files) for KEYS." (interactive (list (list (citar-select-ref)))) - (when (and citar-library-paths - (stringp citar-library-paths)) - (message "Make sure 'citar-library-paths' is a list of paths")) (let* ((embark-default-action-overrides '((multi-category . citar--open-multi) (file . citar-file-open) (url . browse-url))) - (key-entry-alist (citar--ensure-entries keys-entries)) - (files - (citar-file--files-for-multiple-entries - keys - (append citar-library-paths citar-notes-paths) - ;; find files with any extension: - nil)) - (links - (seq-map - (lambda (key) - (citar--get-link key)) - keys)) + (files (let ((citar-library-file-extensions nil)) + (citar-get-files keys))) + (links (citar-get-links keys)) (resource-candidates (delete-dups (append files (remq nil links))))) (cond ((eq nil resource-candidates) @@ -1071,38 +1058,32 @@ For use with `embark-act-all'." (find-file selection)) (t (citar-file-open selection)))) -(defun citar--library-file-action (key action) - "Run ACTION on file associated with KEY." - (let* ((fn (pcase action - ('open 'citar-file-open) - ('attach 'mml-attach-file))) - (entry (citar--get-entry key)) - (files - (citar-file--files-for-entry - key - entry - citar-library-paths - citar-library-file-extensions)) - (file - (pcase (length files) - (1 (car files)) - ((guard (> 1)) - (citar--select-resource files))))) - (if file - (funcall fn file) - (message "No associated file")))) +;; TODO Rename? This also opens files in bib field, not just library files +;;;###autoload +(defun citar-open-files (key-or-keys) + "Open library file associated with KEY-OR-KEYS." + (interactive (list (citar-select-refs))) + ;; TODO filter to refs have files? + (let ((embark-default-action-overrides '((file . citar-file-open)))) + (citar--library-file-action key-or-keys #'citar-file-open))) ;;;###autoload -(defun citar-open-library-file (key-entry) - "Open library file associated with the KEY-ENTRY. +(defun citar-attach-library-file (key) + "Attach library file associated with KEY to outgoing MIME message. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) - (let ((embark-default-action-overrides '((file . citar-file-open)))) - (when (and citar-library-paths - (stringp citar-library-paths)) - (error "Make sure 'citar-library-paths' is a list of paths")) - (citar--library-file-action key 'open))) + (let ((embark-default-action-overrides '((file . mml-attach-file)))) + (citar--library-file-action key #'mml-attach-file))) + +(defun citar--library-file-action (key-or-keys action) + "Run ACTION on file associated with KEY." + (if-let* ((files (citar-get-files key-or-keys)) + (file (if (null (cdr files)) + (car files) + (citar--select-resource files)))) + (funcall action file) + (message "No associated files for %s" key-or-keys))) ;;;###autoload (defun citar-open-notes (key) @@ -1111,7 +1092,7 @@ With prefix, rebuild the cache before offering candidates." ;; REVIEW KEY, or KEYS (interactive (list (citar-select-ref))) (let* ((embark-default-action-overrides '((file . find-file))) - (entry (citar--get-entry key))) + (entry (citar-get-entry key))) (if (listp citar-open-note-functions) (citar--open-notes (car key) entry) (error "Please change the value of 'citar-open-note-functions' to a list")))) @@ -1129,7 +1110,7 @@ With prefix, rebuild the cache before offering candidates." With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) (when-let ((bibtex-files (citar--bibliography-files))) - (bibtex-search-entry (car key) t nil t))) + (bibtex-search-entry key t nil t))) ;;;###autoload (defun citar-insert-bibtex (keys) @@ -1171,17 +1152,15 @@ directory as current buffer." (citar--insert-bibtex key))))) ;;;###autoload -(defun citar-open-link (key) - "Open URL or DOI link associated with the KEY in a browser. - -With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) - (let* ((entry (citar--get-entry key)) - (link (citar--get-link entry))) - (if link - (browse-url link) - (message "No link found for %s" key)))) +(defun citar-open-links (key-or-keys) + "Open URL or DOI link associated with the KEY in a browser." + (interactive (list (citar-select-ref))) + (if-let* ((links (citar-get-links key-or-keys)) + (link (if (null (cdr links)) + (car links) + (citar--select-resource nil links)))) + (browse-url link) + (message "No link found for %s" key-or-keys))) ;;;###autoload (defun citar-insert-citation (keys &optional arg) @@ -1191,11 +1170,8 @@ Prefix ARG is passed to the mode-specific insertion function. It should invert the default behaviour for that mode with respect to citation styles. See specific functions for more detail." (interactive - (if (member major-mode (mapcar - 'caar - (butlast citar-major-mode-functions))) - (list (citar-select-refs) ; key-entries - current-prefix-arg) ; arg + (if (citar--get-major-mode-function 'insert-citation) + (list (citar-select-refs) current-prefix-arg) (error "Citation insertion is not supported for %s" major-mode))) (citar--major-mode-function 'insert-citation @@ -1233,7 +1209,7 @@ ARG is forwarded to the mode-specific insertion function given in (defun citar-format-reference (keys) "Return formatted reference(s) for the elements of KEYS." - (let* ((entries (mapcar #'citar--get-entry keys)) + (let* ((entries (mapcar #'citar-get-entry keys)) (template (citar--get-template 'preview))) (with-temp-buffer (dolist (entry entries) @@ -1244,8 +1220,7 @@ ARG is forwarded to the mode-specific insertion function given in (defun citar-insert-keys (keys) "Insert KEYS citekeys. With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-refs - :rebuild-cache current-prefix-arg))) + (interactive (list (citar-select-refs))) (citar--major-mode-function 'insert-keys #'citar--insert-keys-comma-separated @@ -1255,22 +1230,11 @@ With prefix, rebuild the cache before offering candidates." "Insert comma separated KEYS." (insert (string-join keys ", "))) -;;;###autoload -(defun citar-attach-library-file (key) - "Attach library file associated with KEY to outgoing MIME message. - -With prefix, rebuild the cache before offering candidates." - (interactive (list (citar-select-ref))) - (let ((embark-default-action-overrides '((file . mml-attach-file)))) - (when (and citar-library-paths - (stringp citar-library-paths)) - (error "Make sure 'citar-library-paths' is a list of paths")) - (citar--library-file-action key-entry 'attach))) - (defun citar--add-file-to-library (key) "Add a file to the library for KEY. The FILE can be added from an open buffer, a file path, or a URL." + (citar--check-configuration 'citar-library-paths) (let* ((source (char-to-string (read-char-choice @@ -1303,9 +1267,8 @@ URL." "Add a file to the library for KEY. The FILE can be added either from an open buffer, a file, or a URL." - (interactive (list (citar-select-ref - :rebuild-cache current-prefix-arg))) - (citar--add-file-to-library (car key-entry))) + (interactive (list (citar-select-ref))) + (citar--add-file-to-library key)) ;;;###autoload (defun citar-run-default-action (keys) From 4a84e66b23c9638c4e8a16e4e80a41a7c919e533 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 22:02:24 -0600 Subject: [PATCH 40/78] Fix filter handling in citar-select-refs --- citar.el | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/citar.el b/citar.el index 85a40b3c..af146543 100644 --- a/citar.el +++ b/citar.el @@ -482,11 +482,9 @@ and other completion functions." ;; REVIEW this now works, but probably needs refinement (let ((predicate (when (or filter predicate) - (lambda (cand _) - (let* ((key (gethash cand candidates)) - (entry (citar-get-entry key))) - (and (or (null filter) (funcall filter key entry)) - (or (null predicate) (funcall predicate string)))))))) + (lambda (_ key) + (and (or (null filter) (funcall filter key)) + (or (null predicate) (funcall predicate string))))))) (complete-with-action action candidates string predicate)))))) (cl-defun citar-select-refs (&key (multiple t) filter) From d615ae49b89210a84b792d52e51a2674fc0f657a Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 22:04:10 -0600 Subject: [PATCH 41/78] Mask out citar-filenotify.el in Eldev --- Eldev | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Eldev b/Eldev index a1223d7f..32678d37 100755 --- a/Eldev +++ b/Eldev @@ -8,7 +8,8 @@ (eldev-use-plugin 'autoloads) -(setf eldev-standard-excludes `(:or ,eldev-standard-excludes "./test/manual/" "./citar-capf.el")) +;; TODO what to do with these excluded files? +(setf eldev-standard-excludes `(:or ,eldev-standard-excludes "./test/manual/" "./citar-capf.el" "./citar-filenotify.el")) ;; (setf eldev-test-fileset '("./test/" "!./test/manual/")) (eldev-add-extra-dependencies '(build test lint) 'embark 'auctex) From 633274850e12c323e7903a433895a7c24d725b91 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 22:40:07 -0600 Subject: [PATCH 42/78] Add `citar-key-at-point` and `citar-citation-at-point` functions. Fixes #642. --- citar.el | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/citar.el b/citar.el index af146543..ff778fb1 100644 --- a/citar.el +++ b/citar.el @@ -667,6 +667,22 @@ Return nil if there are no bibliography files or no entries." (read candidate) (substring-no-properties candidate 0 (cl-position ?\s candidate))))) +(defun citar-key-at-point () + "Return the citation key at point in the current buffer. +Return nil if there is no key at point or the major mode is not +supported." + (when-let ((keywithbounds (citar--major-mode-function 'key-at-point #'ignore))) + (if (consp keywithbounds) + (car keywithbounds) ; take just key, not bounds + keywithbounds))) + +(defun citar-citation-at-point () + "Return a list of keys comprising the citation at point in the current buffer. +Return nil if there is no key at point or the major mode is not + supported." + (when-let ((citationwithbounds (citar--major-mode-function 'citation-at-point #'ignore))) + (car citationwithbounds))) + ;;; Major-mode functions (defun citar--get-major-mode-function (key &optional default) From 55bc450116a411122f3c9ff00da954c8ebdb0c37 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Thu, 23 Jun 2022 23:01:53 -0600 Subject: [PATCH 43/78] Update docstrings for new API; add missing docstrings. --- citar-cache.el | 12 +++++++++--- citar-citeproc.el | 2 +- citar-format.el | 4 ++++ citar.el | 23 ++++++++++++++--------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/citar-cache.el b/citar-cache.el index 4be85b8e..a9a946d9 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -102,19 +102,25 @@ element of FILENAMES." filenames)) (defun citar-cache--entry (key bibs) + "Find the first entry for KEY in the bibliographies BIBS. +BIBS should be a list of `citar-cache--bibliography' objects." (catch :found - ;; Iterate through the cached bibliography hashes and find a key. (dolist (bib bibs) (let* ((entries (citar-cache--bibliography-entries bib)) (entry (gethash key entries))) - (when entry (throw :found entry)))) - nil)) + (when entry (throw :found entry)))))) (defun citar-cache--entries (bibs) + "Return hash table containing merged entries of BIBS. +BIBS should be a list of `citar-cache--bibliography' objects. If +a key is present in multiple bibliographies in BIBS, keep the +entry that appears first. Return a hash table mapping the keys of +all BIBS to their entries." (apply #'map-merge '(hash-table :test equal) (nreverse (mapcar #'citar-cache--bibliography-entries bibs)))) (defun citar-cache--preformatted (bibs) + "Return hash table containing pre-formatted strings from BIBS." (apply #'map-merge '(hash-table :test equal) (nreverse (mapcar #'citar-cache--bibliography-preformatted bibs)))) diff --git a/citar-citeproc.el b/citar-citeproc.el index c2b1d947..bb44e55d 100644 --- a/citar-citeproc.el +++ b/citar-citeproc.el @@ -97,7 +97,7 @@ accepted.") ;;;###autoload (defun citar-citeproc-format-reference (keys) - "Return formatted reference(s) for KEYS-ENTRIES via `citeproc-el`. + "Return formatted reference(s) for KEYS via `citeproc-el`. Formatting follows CSL style set in `citar-citeproc-csl-style`. With prefix-argument, select CSL style." (when (or (eq citar-citeproc-csl-style nil) diff --git a/citar-format.el b/citar-format.el index 8679b0b8..f7a5ca82 100644 --- a/citar-format.el +++ b/citar-format.el @@ -48,6 +48,10 @@ (defun citar-format--preformat (fieldspecs entry hide-elided ellipsis) + "Pre-format ENTRY using parsed format string FIELDSPECS. +FIELDSPECS should be the result of `citar-format--parse'. See the +documentation of `citar-format--string' for the meaning of +HIDE-ELIDED and ELLIPSIS." (let ((preformatted nil) (fields "") (width 0)) diff --git a/citar.el b/citar.el index ff778fb1..1d06d790 100644 --- a/citar.el +++ b/citar.el @@ -624,10 +624,15 @@ HISTORY is the `completing-read' history argument." (cl-defun citar--format-candidates (&key (bibs (citar--bibliographies)) (entries (citar-cache--entries bibs))) - "Return completion table for cite keys, as a hash table. -In this hash table, keys are a strings with author, date, and -title of the reference. Values are the cite keys. -Return nil if there are no bibliography files or no entries." + "Format completion candidates for ENTRIES. + +BIBS should be a list of `citar-cache--bibliography' objects that +are the source of ENTRIES. Use the pre-formatted strings in BIBS +to format candidates. + +Return a hash table with the keys being completion candidate +strings and values being citation keys. Return nil if BIBS is +nil." ;; Populate bibliography cache. (when bibs (let* ((preformatted (citar-cache--preformatted bibs)) @@ -1082,16 +1087,16 @@ For use with `embark-act-all'." (citar--library-file-action key-or-keys #'citar-file-open))) ;;;###autoload -(defun citar-attach-library-file (key) - "Attach library file associated with KEY to outgoing MIME message. +(defun citar-attach-library-file (key-or-keys) + "Attach library file associated with KEY-OR-KEYS to outgoing MIME message. With prefix, rebuild the cache before offering candidates." (interactive (list (citar-select-ref))) (let ((embark-default-action-overrides '((file . mml-attach-file)))) - (citar--library-file-action key #'mml-attach-file))) + (citar--library-file-action key-or-keys #'mml-attach-file))) (defun citar--library-file-action (key-or-keys action) - "Run ACTION on file associated with KEY." + "Run ACTION on file associated with KEY-OR-KEYS." (if-let* ((files (citar-get-files key-or-keys)) (file (if (null (cdr files)) (car files) @@ -1167,7 +1172,7 @@ directory as current buffer." ;;;###autoload (defun citar-open-links (key-or-keys) - "Open URL or DOI link associated with the KEY in a browser." + "Open URL or DOI link associated with KEY-OR-KEYS in a browser." (interactive (list (citar-select-ref))) (if-let* ((links (citar-get-links key-or-keys)) (link (if (null (cdr links)) From d75732dbfdd11fc77de53a5ccea250499592266d Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 24 Jun 2022 01:15:08 -0600 Subject: [PATCH 44/78] Manually merge changes by @bdarcus. --- citar-embark.el | 175 +++++++++++++++++++++--------------------------- citar.el | 22 +++++- 2 files changed, 94 insertions(+), 103 deletions(-) diff --git a/citar-embark.el b/citar-embark.el index 50af6eef..caa498d1 100644 --- a/citar-embark.el +++ b/citar-embark.el @@ -1,25 +1,21 @@ -;;; citar-embark.el --- Integrate citar with embark -*- lexical-binding: t; -*- +;;; citar-embark.el --- Citar/Embark integration -*- lexical-binding: t; -*- ;; -;; Copyright (C) 2021 Bruce D'Arcus +;; Copyright (C) 2022 Bruce D'Arcus ;; -;; This file is not part of GNU Emacs. +;; Author: Bruce D'Arcus +;; Maintainer: Bruce D'Arcus +;; Created: June 22, 2022 +;; Modified: June 22, 2022 +;; Version: 1.0 +;; Keywords: bib extensions +;; Homepage: https://github.com/emacs-citar/citar-embark +;; Package-Requires: ((emacs "27.2") (embark "0.17") (citar "0.9.5")) ;; -;; This program is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . +;; This file is not part of GNU Emacs. ;; ;;; Commentary: ;; -;; Add embark functionality to citar. +;; Description ;; ;;; Code: @@ -35,18 +31,40 @@ (defvar citar-embark-citation-map (make-composed-keymap citar-citation-map nil) "Keymap for Embark actions on Citar citations and keys.") -;;; At-point functions for Embark +;;;; Variables + +(defvar citar-embark--target-finders + (list #'citar-embark--key-finder + #'citar-embark--citation-finder)) + +(defvar citar-embark--candidate-collectors + (list #'citar-embark--selected)) + +(defvar citar-embark--transformer-alist + (list (cons 'citar-candidate #'citar-embark--candidate-transformer))) + +(defvar citar-embark--keymap-alist + '((citar-reference . citar-embark-map) ; minibuffer candidates + (citar-key . citar-embark-citation-map) ; at-point keys + (citar-citation . citar-embark-citation-map))) ; at-point citations + +(defvar citar-embark--multitarget-actions + (list #'citar-insert-bibtex #'citar-insert-citation #'citar-insert-reference + #'citar-copy-reference #'citar-insert-keys #'citar-run-default-action)) + +(defvar citar-embark--target-injection-hooks + (list (list #'citar-insert-edit #'embark--ignore-target))) + +;;;; At-point functions for Embark (defun citar-embark--key-finder () "Return the citation key at point." - (when-let (key (and (not (minibufferp)) - (citar--major-mode-function 'key-at-point #'ignore))) + (when-let ((key (and (not (minibufferp)) (citar--key-at-point)))) (cons 'citar-key key))) (defun citar-embark--citation-finder () "Return the keys of the citation at point." - (when-let (citation (and (not (minibufferp)) - (citar--major-mode-function 'citation-at-point #'ignore))) + (when-let ((citation (and (not (minibufferp)) (citar--citation-at-point)))) `(citar-citation ,(citar--stringify-keys (car citation)) . ,(cdr citation)))) (defun citar-embark--candidate-transformer (_type target) @@ -66,90 +84,47 @@ (funcall minibuffer-completion-predicate cand))))))) (cons (completion-metadata-get metadata 'category) cands))) +;;;; Enable and disable Citar/Embark integration + +(defun citar-embark--enable () + "Add Citar-specific functions and keymaps to Embark." + (mapc (apply-partially #'add-hook 'embark-target-finders) + (reverse citar-embark--target-finders)) + (mapc (apply-partially #'add-hook 'embark-candidate-collectors) + (reverse citar-embark--candidate-collectors)) + (pcase-dolist (`(,type . ,transformer) citar-embark--transformer-alist) + (setf (alist-get type embark-transformer-alist) transformer)) + (pcase-dolist (`(,type . ,keymap) citar-embark--keymap-alist) + (setf (alist-get type embark-keymap-alist) keymap)) + (cl-callf cl-union embark-multitarget-actions citar-embark--multitarget-actions) + (pcase-dolist (`(,action . ,hooks) citar-embark--target-injection-hooks) + (cl-callf cl-union (alist-get action embark-target-injection-hooks) hooks))) + +(defun citar-embark--disable () + "Undo the effects of `citar-embark--enable'." + (mapc (apply-partially #'remove-hook 'embark-target-finders) + citar-embark--target-finders) + (mapc (apply-partially #'remove-hook 'embark-candidate-collectors) + citar-embark--candidate-collectors) + (cl-callf cl-set-difference embark-transformer-alist citar-embark--transformer-alist :test #'equal) + (cl-callf cl-set-difference embark-keymap-alist citar-embark--keymap-alist :test #'equal) + (cl-callf cl-set-difference embark-multitarget-actions citar-embark--multitarget-actions) + (pcase-dolist (`(,action . ,hooks) citar-embark--target-injection-hooks) + (when-let ((alistentry (assq action embark-target-injection-hooks))) + (cl-callf cl-set-difference (cdr alistentry) hooks) + (unless (cdr alistentry) ; if no other hooks, remove alist entry + (cl-callf2 remq alistentry embark-target-injection-hooks))))) + ;;;###autoload (define-minor-mode citar-embark-mode - "Toggle Citar target finders for Embark." + "Toggle integration between Citar and Embark." :group 'citar :global t :init-value nil - :lighter nil - (let ((targetfinders (list #'citar-embark--key-finder #'citar-embark--citation-finder)) - (collectors (list #'citar-embark--selected)) - (transformers (list (cons 'citar-candidate #'citar-embark--candidate-transformer))) - (keymaps '((citar-reference . citar-embark-map) ; minibuffer candidates - (citar-key . citar-embark-citation-map) ; at-point keys - (citar-citation . citar-embark-citation-map))) - (multitarget (list #'citar-insert-bibtex #'citar-insert-citation - #'citar-insert-reference #'citar-copy-reference - #'citar-insert-keys #'citar-run-default-action)) - (ignoretarget (list #'citar-insert-edit))) ; at-point citations - (if citar-embark-mode - (progn - ;; Add target finders for `embark-act' - (dolist (targetfinder (reverse targetfinders)) - (add-hook 'embark-target-finders targetfinder)) - - ;; Add collectors for `embark-collect', `embark-act-all', etc. - (dolist (collector (reverse collectors)) - (add-hook 'embark-candidate-collectors collector)) - - ;; Add target transformers - (dolist (transformer transformers) - (setf (alist-get (car transformer) embark-transformer-alist) (cdr transformer))) - - ;; Add Embark keymaps - (dolist (keymap keymaps) - (setf (alist-get (car keymap) embark-keymap-alist) (cdr keymap))) - - ;; Mark commands as multitarget actions - (dolist (command multitarget) - (cl-pushnew command embark-multitarget-actions)) - - ;; Mark commands as ignoring target - (dolist (command ignoretarget) - (cl-pushnew #'embark--ignore-target - (alist-get command (if (boundp 'embark-setup-action-hooks) - ;; TODO Remove backward compatibility for Embark < 0.15? - embark-setup-action-hooks - embark-target-injection-hooks))))) - ;; Disable citar-embark-mode: - - ;; Remove target finders - (dolist (targetfinder targetfinders) - (remove-hook 'embark-target-finders targetfinder)) - - ;; Remove target collectors - (dolist (collector collectors) - (remove-hook 'embark-candidate-collectors collector)) - - ;; Remove target transformers - (dolist (transformer transformers) - (cl-callf2 assq-delete-all (car transformer) embark-transformer-alist)) - - ;; Remove Embark keymaps - (dolist (keymap keymaps) - (cl-callf2 assq-delete-all (car keymap) embark-transformer-alist)) - - ;; Remove commands from embark-multitarget-actions - (cl-callf cl-set-difference embark-multitarget-actions multitarget) - - ;; Remove #'embark--ignore-target setup hook - (dolist (command ignoretarget) - ;; TODO simplfy this when we drop compatibility with Embark < 0.15 - (cl-callf (lambda (hookalist) - (when-let ((alistentry (assq command hookalist))) - (cl-callf2 remq #'embark--ignore-target (cdr alistentry)) - (unless (cdr alistentry) ; if no other hooks, remove alist entry - (cl-callf2 remq alistentry hookalist))) - hookalist) - (if (boundp 'embark-setup-action-hooks) - embark-setup-action-hooks - embark-target-injection-hooks)))))) - -;;;###autoload -(with-eval-after-load 'citar - (with-eval-after-load 'embark - (citar-embark-mode))) + :lighter " citar-embark" + (if citar-embark-mode + (citar-embark--enable) + (citar-embark--disable))) (provide 'citar-embark) ;;; citar-embark.el ends here diff --git a/citar.el b/citar.el index 1d06d790..1fef94df 100644 --- a/citar.el +++ b/citar.el @@ -672,11 +672,28 @@ nil." (read candidate) (substring-no-properties candidate 0 (cl-position ?\s candidate))))) +(defun citar--key-at-point () + "Return bibliography key at point in current buffer, along with its bounds. +Return either a string KEY or a cons pair (KEY . BOUNDS), where +BOUNDS is a (BEG . END) pair indicating the location of KEY in +the buffer. Return nil if there is no key at point or the current +major mode is not supported." + (citar--major-mode-function 'key-at-point #'ignore)) + +(defun citar--citation-at-point () + "Return citation at point in current buffer, along with its bounds. +Return (KEYS . BOUNDS), where KEYS is a list of citation keys and +BOUNDS is a (BEG . END) pair indicating the location of the +citation in the buffer. BOUNDS may be nil if the location cannot +be determined. Return nil if there is no citation at point or the +current major mode is not supported." + (citar--major-mode-function 'citation-at-point #'ignore)) + (defun citar-key-at-point () "Return the citation key at point in the current buffer. Return nil if there is no key at point or the major mode is not supported." - (when-let ((keywithbounds (citar--major-mode-function 'key-at-point #'ignore))) + (when-let ((keywithbounds (citar--key-at-point))) (if (consp keywithbounds) (car keywithbounds) ; take just key, not bounds keywithbounds))) @@ -685,8 +702,7 @@ supported." "Return a list of keys comprising the citation at point in the current buffer. Return nil if there is no key at point or the major mode is not supported." - (when-let ((citationwithbounds (citar--major-mode-function 'citation-at-point #'ignore))) - (car citationwithbounds))) + (car (citar--citation-at-point))) ;;; Major-mode functions From 4093b7a8f4e41efdd0200582968f87016ce602aa Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 24 Jun 2022 01:33:43 -0600 Subject: [PATCH 45/78] Fix docstring quoting style. The Emacs manual specifies using either curly quotes or `...'. The Emacs 29 byte compiler complains about unescaped single quotes. Also replace the incorrect `...` style. --- citar-cache.el | 52 ++++++++++++++++++--------------------- citar-capf.el | 2 +- citar-citeproc.el | 8 +++--- citar-file.el | 2 +- citar-latex.el | 4 +-- citar-markdown.el | 6 ++--- citar-org.el | 6 ++--- citar.el | 8 +++--- test/citar-format-test.el | 2 +- 9 files changed, 43 insertions(+), 47 deletions(-) diff --git a/citar-cache.el b/citar-cache.el index a9a946d9..da3e8256 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -39,12 +39,8 @@ (defvar citar-cache--bibliographies (make-hash-table :test 'equal) "Cache for parsed bibliography files. -This is an association list following the pattern: - (FILE-ID . ENTRIES) -FILE-ID is a cons cell (FILE . HASH), with FILE being the absolute file name of -the bibliography file, and HASH a hash of its contents. -ENTRIES is a hash table with citation references as keys and fields alist as -values.") +This is a hash table with keys being file names and the values +being `citar-cache--bibliography' objects.") ;;; Bibliography objects @@ -58,11 +54,11 @@ values.") nil :read-only t :documentation - "True filename of a bibliography, as returned by `file-truename`.") + "True filename of a bibliography, as returned by `file-truename'.") (hash nil :documentation - "Hash of the file's contents, as returned by `buffer-hash`.") + "Hash of the file's contents, as returned by `buffer-hash'.") (buffers nil :documentation @@ -71,12 +67,12 @@ values.") (make-hash-table :test 'equal) :documentation "Hash table mapping citation keys to bibliography entries, - as returned by `parsebib-parse`.") + as returned by `parsebib-parse'.") (preformatted (make-hash-table :test 'equal) :documentation "Pre-formatted strings used to display bibliography entries; - see `citar--preformatter`.") + see `citar--preformatter'.") (format-string nil :documentation @@ -85,16 +81,16 @@ values.") (defun citar-cache--get-bibliographies (filenames &optional buffer) "Return cached bibliographies for FILENAMES and associate them with BUFFER. -FILENAMES is a list of bibliography file names. If BUFFER is -nil, use the current buffer. Otherwise, BUFFER should be a -buffer object or name that requires these bibliographies, or a -symbol like 'global. +FILENAMES is a list of bibliography file names. If BUFFER is nil, +use the current buffer. Otherwise, BUFFER should be a buffer +object or name that requires these bibliographies, or a symbol +like `global'. Remove any existing associations between BUFFER and cached files -not included in FILENAMES. Release cached files that are no +not included in FILENAMES. Release cached files that are no longer needed by any other buffer. -Return a list of `citar--bibliography` objects, one for each +Return a list of `citar--bibliography' objects, one for each element of FILENAMES." (citar-cache--release-bibliographies filenames buffer) (mapcar @@ -130,10 +126,10 @@ all BIBS to their entries." (defun citar-cache--get-bibliography (filename &optional buffer) "Return cached bibliography for FILENAME and associate it with BUFFER. -If FILENAME is not already cached, read and cache it. If BUFFER -is nil, use the current buffer. Otherwise, BUFFER should be a +If FILENAME is not already cached, read and cache it. If BUFFER +is nil, use the current buffer. Otherwise, BUFFER should be a buffer object or name that requires the bibliography FILENAME, or -a symbol like 'global." +a symbol like `global'." (let* ((cached (gethash filename citar-cache--bibliographies)) (bib (or cached (citar-cache--make-bibliography filename))) (buffer (citar-cache--canonicalize-buffer buffer)) @@ -159,10 +155,10 @@ a symbol like 'global." (defun citar-cache--release-bibliographies (&optional keep-filenames buffer) "Dissociate BUFFER from cached bibliographies. -If BUFFER is nil, use the current buffer. Otherwise, BUFFER -should be a buffer object, buffer name, or a symbol like 'global. -KEEP-FILENAMES is a list of file names that are not dissociated -from BUFFER. +If BUFFER is nil, use the current buffer. Otherwise, BUFFER +should be a buffer object, buffer name, or a symbol like +`global'. KEEP-FILENAMES is a list of file names that are not +dissociated from BUFFER. Remove any bibliographies from the cache that are no longer needed by any other buffer." @@ -196,7 +192,7 @@ modified since the last time BIB was updated." (insert-file-contents filename) (buffer-hash)))) ;; TODO Also check file size and modification time before hashing? - ;; See `file-has-changed-p` in emacs 29, or `org-file-has-changed-p` + ;; See `file-has-changed-p' in emacs 29, or `org-file-has-changed-p` (when (or force (not (equal newhash (citar-cache--bibliography-hash bib)))) ;; Update entries (clrhash entries) @@ -236,10 +232,10 @@ modified since the last time BIB was updated." (defun citar-cache--canonicalize-buffer (buffer) "Return buffer object or symbol denoted by BUFFER. -If BUFFER is nil, return the current buffer. Otherwise, BUFFER -should be a buffer object or name, or a symbol like 'global. If -it is a buffer object or symbol, it is returned as-is. -Otherwise, return the buffer object whose name is BUFFER." +If BUFFER is nil, return the current buffer. Otherwise, BUFFER +should be a buffer object or name, or a symbol like `global'. If +it is a buffer object or symbol, it is returned as-is. Otherwise, +return the buffer object whose name is BUFFER." (cond ((null buffer) (current-buffer)) ((symbolp buffer) buffer) (t (get-buffer buffer)))) diff --git a/citar-capf.el b/citar-capf.el index cb17b154..3dcc19f7 100644 --- a/citar-capf.el +++ b/citar-capf.el @@ -46,7 +46,7 @@ ;;;; Citar-Capf ;;;###autoload (defun citar-capf () - "Citation key `completion-at-point` for org, markdown, or latex." + "Citation key `completion-at-point' for org, markdown, or latex." (let ((citar-capf-latex-regexp "\\(?:cite\\(?:\\(?:[pt]\\*\\|[pt]\\)?{\\)\\)\\([[:alnum:]_-]*,\\)*\\([[:alnum:]_-]*\\)") (citar-capf-markdown-regexp diff --git a/citar-citeproc.el b/citar-citeproc.el index bb44e55d..02d96bbb 100644 --- a/citar-citeproc.el +++ b/citar-citeproc.el @@ -65,7 +65,7 @@ :type 'directory) (defvar citar-citeproc-csl-style nil - "CSL style file to be used with `citar-citeproc-format-reference`. + "CSL style file to be used with `citar-citeproc-format-reference'. If file is located in the directory set to `citar-citeproc-csl-styles-dir', only the filename itself is @@ -82,7 +82,7 @@ accepted.") ;;;###autoload (defun citar-citeproc-select-csl-style () - "Select CSL style to be used with `citar-citeproc-format-reference`." + "Select CSL style to be used with `citar-citeproc-format-reference'." (interactive) (unless citar-citeproc-csl-styles-dir (error "Be sure to set 'citar-citeproc-csl-styles-dir' to your CSL styles directory")) @@ -97,8 +97,8 @@ accepted.") ;;;###autoload (defun citar-citeproc-format-reference (keys) - "Return formatted reference(s) for KEYS via `citeproc-el`. -Formatting follows CSL style set in `citar-citeproc-csl-style`. + "Return formatted reference(s) for KEYS via `citeproc-el'. +Formatting follows CSL style set in `citar-citeproc-csl-style'. With prefix-argument, select CSL style." (when (or (eq citar-citeproc-csl-style nil) current-prefix-arg) diff --git a/citar-file.el b/citar-file.el index 1612caa8..1a31471b 100644 --- a/citar-file.el +++ b/citar-file.el @@ -69,7 +69,7 @@ (defcustom citar-file-note-extensions '("org" "md") "List of file extensions to filter for notes. -These are the extensions the `citar-open-note-function` +These are the extensions the `citar-open-note-function' will open, via `citar-open-notes'." :group 'citar :type '(repeat string)) diff --git a/citar-latex.el b/citar-latex.el index 7c576827..8b1ba3a8 100644 --- a/citar-latex.el +++ b/citar-latex.el @@ -172,7 +172,7 @@ whether or not to prompt. The availiable commands and how to provide them arguments are configured by `citar-latex-cite-commands'. -If `citar-latex-prompt-for-extra-arguments' is `nil`, every +If `citar-latex-prompt-for-extra-arguments' is nil, every command is assumed to have a single argument into which keys are inserted." (when keys @@ -217,7 +217,7 @@ With ARG non-nil, rebuild the cache before offering candidates." citar-latex-default-cite-command nil)) (defun citar-latex--is-a-cite-command (command) - "Return element of `citar-latex-cite-commands` containing COMMAND." + "Return element of `citar-latex-cite-commands' containing COMMAND." (seq-find (lambda (x) (member command (car x))) citar-latex-cite-commands)) diff --git a/citar-markdown.el b/citar-markdown.el index 84cd02cf..f10300c2 100644 --- a/citar-markdown.el +++ b/citar-markdown.el @@ -66,7 +66,7 @@ If point is immediately after the opening \[, add new keys to the beginning of the citation. If INVERT-PROMPT is non-nil, invert the meaning of -`citar-markdown-prompt-for-extra-arguments`." +`citar-markdown-prompt-for-extra-arguments'." (let* ((citation (citar-markdown-citation-at-point)) (keys (if citation (seq-difference keys (car citation)) keys)) (keyconcat (mapconcat (lambda (k) (concat "@" k)) keys "; ")) @@ -96,7 +96,7 @@ With ARG non-nil, rebuild the cache before offering candidates." "Return citation key at point (with its bounds) for pandoc markdown citations. Returns (KEY . BOUNDS), where KEY is the citation key at point and BOUNDS is a pair of buffer positions. Citation keys are -found using `citar-markdown-citation-key-regexp`. Returns nil if +found using `citar-markdown-citation-key-regexp'. Returns nil if there is no key at point." (interactive) (when (thing-at-point-looking-at citar-markdown-citation-key-regexp) @@ -108,7 +108,7 @@ there is no key at point." "Return keys of citation at point. Find balanced expressions starting and ending with square brackets and containing at least one citation key (matching -`citar-markdown-citation-key-regexp`). Return (KEYS . BOUNDS), +`citar-markdown-citation-key-regexp'). Return (KEYS . BOUNDS), where KEYS is a list of the found citation keys and BOUNDS is a pair of buffer positions indicating the start and end of the citation." diff --git a/citar-org.el b/citar-org.el index 79b18be3..ee008e08 100644 --- a/citar-org.el +++ b/citar-org.el @@ -197,7 +197,7 @@ With PROC list, limit to specific processor(s)." ;;;###autoload (defun citar-org-insert-edit (&optional arg) - "Run `org-cite-insert` with citar insert processor. + "Run `org-cite-insert' with citar insert processor. ARG is used as the prefix argument." (let ((org-cite-insert-processor 'citar)) (org-cite-insert arg))) @@ -263,12 +263,12 @@ strings by style." ;;; Org note function (defun citar-org--id-get-create (&optional force) - "Call `org-id-get-create` while maintaining point. + "Call `org-id-get-create' while maintaining point. If point is at the beginning of the buffer and a new properties drawer is created, move point after the drawer. -More generally, if `org-id-get-create` inserts text at point, +More generally, if `org-id-get-create' inserts text at point, move point after the insertion. With optional argument FORCE, force the creation of a new ID." diff --git a/citar.el b/citar.el index 1fef94df..078b7065 100644 --- a/citar.el +++ b/citar.el @@ -393,7 +393,7 @@ of all citations in the current buffer." (defcustom citar-select-multiple t "Use `completing-read-multiple' for selecting citation keys. -When nil, all citar commands will use `completing-read`." +When nil, all citar commands will use `completing-read'." :type 'boolean :group 'citar) @@ -471,7 +471,7 @@ By default the metadata of the table contains the category and affixation function. METADATA are extra entries for metadata of the form (KEY . VAL). -The returned completion table can be used with `completing-read` +The returned completion table can be used with `completing-read' and other completion functions." (let ((metadata `(metadata . ((category . citar-candidate) . ((affixation-function . ,#'citar--ref-affix) @@ -881,7 +881,7 @@ smaller subset." Return a function that takes KEY and returns non-nil when the corresponding entry in ENTRIES has associated links. See the -documentation of `citar-has-files` and `citar-has-notes', which +documentation of `citar-has-files' and `citar-has-notes', which have similar usage." (citar--has-resources-for-entries entries @@ -1217,7 +1217,7 @@ citation styles. See specific functions for more detail." (defun citar-insert-edit (&optional arg) "Edit the citation at point. ARG is forwarded to the mode-specific insertion function given in -`citar-major-mode-functions`." +`citar-major-mode-functions'." (interactive "P") (citar--major-mode-function 'insert-edit diff --git a/test/citar-format-test.el b/test/citar-format-test.el index 205dfb57..30b62f19 100644 --- a/test/citar-format-test.el +++ b/test/citar-format-test.el @@ -8,7 +8,7 @@ (require 'citar-format) (ert-deftest citar-format-test--star-widths () - "Test `citar-format--star-widths`." + "Test `citar-format--star-widths'." (should (string-empty-p (citar-format--star-widths 80 nil))) From 2938db153e9176c0bca38eb2d63ba9a6a0868ac6 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Fri, 24 Jun 2022 02:34:27 -0600 Subject: [PATCH 46/78] Make `citar-clean-string` and `citar-shorten-names` private. Also add other obsolete function aliases. Rename `citar-attach-library-file` to `citar-attach-file`. --- citar.el | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/citar.el b/citar.el index 078b7065..5642a87f 100644 --- a/citar.el +++ b/citar.el @@ -52,20 +52,26 @@ ;; (make-obsolete 'citar--get-candidates 'citar-get-candidates "1.0") ;; Renamed in 1.0 +(make-obsolete 'citar-has-file #'citar-has-files "1.0") +(make-obsolete 'citar-has-note #'citar-has-notes "1.0") (make-obsolete 'citar-open-library-file #'citar-open-files "1.0") +(make-obsolete 'citar-attach-library-file #'citar-attach-files "1.0") (make-obsolete 'citar-open-link #'citar-open-links "1.0") -(make-obsolete 'citar-get-link "replaced by `citar-get-links'." "1.0") ; now returns list +(make-obsolete 'citar-get-link #'citar-get-links "1.0") ; now returns list ;; make all these private +(make-obsolete 'citar-clean-string 'citar--clean-string "1.0") +(make-obsolete 'citar-shorten-names 'citar--shorten-names "1.0") (make-obsolete 'citar-get-template 'citar--get-template "1.0") -(make-obsolete 'citar-display-value 'citar--display-value "1.0") +(make-obsolete 'citar-display-value 'citar-get-display-value "1.0") (make-obsolete 'citar-open-multi 'citar--open-multi "1.0") (make-obsolete 'citar-select-group-related-resources 'citar--select-group-related-resources "1.0") (make-obsolete 'citar-select-resource 'citar--select-resource "1.0") ;; also rename -(make-obsolete 'citar-has-a-value 'citar-field-with-value "1.0") +(make-obsolete 'citar-has-a-value 'citar-get-field-with-value "0.9.5") ; now returns cons pair +(make-obsolete 'citar-field-with-value 'citar-get-field-with-value "1.0") ; now returns cons pair (make-obsolete 'citar--open-note 'citar-file--open-note "1.0") (make-obsolete-variable @@ -194,8 +200,8 @@ references as a string." (defcustom citar-display-transform-functions ;; TODO change this name, as it might be confusing? - '((t . citar-clean-string) - (("author" "editor") . citar-shorten-names)) + '((t . citar--clean-string) + (("author" "editor") . citar--shorten-names)) "Configure transformation of field display values from raw values. All functions that match a particular field are run in order." @@ -927,12 +933,12 @@ another entry in ENTRIES that has associated resources." ;;; Format and display field values ;; Lifted from bibtex-completion -(defun citar-clean-string (s) +(defun citar--clean-string (s) "Remove quoting brackets and superfluous whitespace from string S." (replace-regexp-in-string "[\n\t ]+" " " (replace-regexp-in-string "[\"{}]+" "" s))) -(defun citar-shorten-names (names) +(defun citar--shorten-names (names) "Return a list of family names from a list of full NAMES. To better accommodate corporate names, this will only shorten @@ -1103,7 +1109,7 @@ For use with `embark-act-all'." (citar--library-file-action key-or-keys #'citar-file-open))) ;;;###autoload -(defun citar-attach-library-file (key-or-keys) +(defun citar-attach-files (key-or-keys) "Attach library file associated with KEY-OR-KEYS to outgoing MIME message. With prefix, rebuild the cache before offering candidates." From 227abceccc264eb4818e6039e4612e46197e3384 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 24 Jun 2022 08:11:26 -0400 Subject: [PATCH 47/78] Update test script --- test/manual/citar.el | 6 ++++-- test/manual/install.el | 11 +++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/test/manual/citar.el b/test/manual/citar.el index cf8deabb..74bb4c42 100644 --- a/test/manual/citar.el +++ b/test/manual/citar.el @@ -10,7 +10,9 @@ ;; activate additional packages we need, including bibtex-actions (require 'embark) (require 'citar) -(require 'consult) +(require 'citar-embark) + +(citar-embark-mode 1) ;; set binding for Embark context menu (global-set-key (kbd "M-;") #'embark-act) @@ -25,7 +27,7 @@ org-cite-activate-processor 'citar)) ;; load the test bib file -(setq citar-bibliography '("test.bib")) +(setq citar-bibliography '("../test.bib")) (setq vertico-count 20) diff --git a/test/manual/install.el b/test/manual/install.el index f6ade1ab..6b4dbfa6 100644 --- a/test/manual/install.el +++ b/test/manual/install.el @@ -14,10 +14,8 @@ (package-install 'load-relative) (package-install 'parsebib) -(package-install 's) -;; completion system options -(package-install 'selectrum) +;; completion (package-install 'vertico) ;; completion style @@ -32,10 +30,11 @@ ;; citar ;; Modify load path so that requires in citar.el are handled -(add-to-list 'load-path "../") +(add-to-list 'load-path "../../") ;; we load this locally, to facilitate development testing on branches -(load-relative "../citar.el") -(load-relative "../citar-org.el") +(load-relative "../../citar.el") +(load-relative "../../citar-org.el") +(load-relative "../../citar-embark.el") ;; theme that supports selectrum and vertico (package-install 'modus-themes) From bc24decfa61bda56da75a32dba2949bde80feab5 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 24 Jun 2022 08:33:39 -0400 Subject: [PATCH 48/78] Update README to include citar-embark --- README.org | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.org b/README.org index 492b6cab..3e3f580b 100644 --- a/README.org +++ b/README.org @@ -73,9 +73,17 @@ This is the minimal configuration, and will work with any completing-read compli *** Embark -Citar will automatically integrate with Embark if it is installed, offering contextual access to actions in the minibuffer and at-point. +The =citar-embark= package adds contextual access actions in the minibuffer and at-point. When using Embark, the Citar actions are generic, and work the same across org, markdown, and latex modes. +#+BEGIN_SRC emacs-lisp +(use-package citar-embark + :bind (("M-;" . embark-act) + ("M-," . embark-dwim)) + :custom + (citar-embark 1)) +#+END_SRC + *** Org-Cite #+CAPTION: org-cite at-point integration with =embark-act= From 68636a77fac7f5c75238ac61e464a1edab758acb Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 26 Jun 2022 06:41:19 -0400 Subject: [PATCH 49/78] Simplify open-note Since the API is key-focused again; adjust the note functions accordingly. --- citar-org.el | 8 ++++++-- citar.el | 16 +++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/citar-org.el b/citar-org.el index ee008e08..3b095d61 100644 --- a/citar-org.el +++ b/citar-org.el @@ -289,11 +289,15 @@ With optional argument FORCE, force the creation of a new ID." (ignore-errors (org-roam-ref-add (concat "@" key))))) ;;;###autoload -(defun citar-org-format-note-default (key entry filepath) - "Format a note FILEPATH from KEY and ENTRY." +(defun citar-org-format-note-default (key) + "Format a note from KEY." (let* ((template (citar--get-template 'note)) + (entry (citar-get-entry key)) (note-meta (when template (citar-format--entry template entry))) + (filepath (expand-file-name + (concat key ".org") + (car citar-notes-paths))) (buffer (find-file filepath))) (with-current-buffer buffer ;; This just overrides other template insertion. diff --git a/citar.el b/citar.el index 5642a87f..f4ca5597 100644 --- a/citar.el +++ b/citar.el @@ -1128,22 +1128,20 @@ With prefix, rebuild the cache before offering candidates." ;;;###autoload (defun citar-open-notes (key) - "Open notes associated with the KEY. -With prefix, rebuild the cache before offering candidates." + "Open notes associated with the KEY." ;; REVIEW KEY, or KEYS (interactive (list (citar-select-ref))) - (let* ((embark-default-action-overrides '((file . find-file))) - (entry (citar-get-entry key))) + (let ((embark-default-action-overrides '((file . find-file)))) (if (listp citar-open-note-functions) - (citar--open-notes (car key) entry) + (citar--open-notes key) (error "Please change the value of 'citar-open-note-functions' to a list")))) -(defun citar--open-notes (key entry) - "Open note(s) associated with KEY and ENTRY." +(defun citar--open-notes (key) + "Open note(s) associated with KEY." (or (seq-some (lambda (opener) - (funcall opener key entry)) citar-open-note-functions) - (funcall citar-create-note-function key entry))) + (funcall opener key)) citar-open-note-functions) + (funcall citar-create-note-function key))) ;;;###autoload (defun citar-open-entry (key) From 6f1ca217092acb6c558e9d6ea76011fd2c9184b2 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 26 Jun 2022 11:44:21 -0600 Subject: [PATCH 50/78] Remove dependency on crm Co-authored-by: Bruce D'Arcus --- citar.el | 1 - 1 file changed, 1 deletion(-) diff --git a/citar.el b/citar.el index f4ca5597..6bd3c64a 100644 --- a/citar.el +++ b/citar.el @@ -44,7 +44,6 @@ (require 'citar-cache) (require 'citar-format) (require 'citar-file) -(require 'crm) ;;; pre-1.0 API cleanup From 4a1325eaf82a35a3232255f45c22a33e45b7c39b Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 26 Jun 2022 11:47:11 -0600 Subject: [PATCH 51/78] Correct docstrings in citar.el. Also tweak obsolete function definitions, and other minor edits. --- citar.el | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/citar.el b/citar.el index 6bd3c64a..730f0764 100644 --- a/citar.el +++ b/citar.el @@ -57,12 +57,12 @@ (make-obsolete 'citar-attach-library-file #'citar-attach-files "1.0") (make-obsolete 'citar-open-link #'citar-open-links "1.0") (make-obsolete 'citar-get-link #'citar-get-links "1.0") ; now returns list +(make-obsolete 'citar-display-value 'citar-get-display-value "1.0") ;; make all these private (make-obsolete 'citar-clean-string 'citar--clean-string "1.0") (make-obsolete 'citar-shorten-names 'citar--shorten-names "1.0") (make-obsolete 'citar-get-template 'citar--get-template "1.0") -(make-obsolete 'citar-display-value 'citar-get-display-value "1.0") (make-obsolete 'citar-open-multi 'citar--open-multi "1.0") (make-obsolete 'citar-select-group-related-resources 'citar--select-group-related-resources "1.0") @@ -679,34 +679,30 @@ nil." (defun citar--key-at-point () "Return bibliography key at point in current buffer, along with its bounds. -Return either a string KEY or a cons pair (KEY . BOUNDS), where -BOUNDS is a (BEG . END) pair indicating the location of KEY in -the buffer. Return nil if there is no key at point or the current +Return (KEY . BOUNDS), where KEY is a string and BOUNDS is either +nil or a (BEG . END) pair indicating the location of KEY in the +buffer. Return nil if there is no key at point or the current major mode is not supported." (citar--major-mode-function 'key-at-point #'ignore)) (defun citar--citation-at-point () "Return citation at point in current buffer, along with its bounds. Return (KEYS . BOUNDS), where KEYS is a list of citation keys and -BOUNDS is a (BEG . END) pair indicating the location of the -citation in the buffer. BOUNDS may be nil if the location cannot -be determined. Return nil if there is no citation at point or the -current major mode is not supported." +BOUNDS is either nil or a (BEG . END) pair indicating the +location of the citation in the buffer. Return nil if there is no +citation at point or the current major mode is not supported." (citar--major-mode-function 'citation-at-point #'ignore)) (defun citar-key-at-point () "Return the citation key at point in the current buffer. Return nil if there is no key at point or the major mode is not supported." - (when-let ((keywithbounds (citar--key-at-point))) - (if (consp keywithbounds) - (car keywithbounds) ; take just key, not bounds - keywithbounds))) + (car (citar--key-at-point))) (defun citar-citation-at-point () "Return a list of keys comprising the citation at point in the current buffer. -Return nil if there is no key at point or the major mode is not - supported." +Return nil if there is no citation at point or the major mode is +not supported." (car (citar--citation-at-point))) ;;; Major-mode functions @@ -1305,6 +1301,7 @@ URL." "Add a file to the library for KEY. The FILE can be added either from an open buffer, a file, or a URL." + ;; Why is there a separate citar--add-file-to-library? (interactive (list (citar-select-ref))) (citar--add-file-to-library key)) @@ -1317,8 +1314,7 @@ URL." (defun citar-dwim () "Run the default action on citation keys found at point." (interactive) - (if-let ((keys (or (car (citar--major-mode-function 'citation-at-point #'ignore)) - (car (citar--major-mode-function 'key-at-point #'ignore))))) + (if-let ((keys (or (citar-key-at-point) (citar-citation-at-point)))) (citar-run-default-action (if (listp keys) keys (list keys))) (user-error "No citation keys found"))) From c5e4455821605a1e15cb78a31dfdb4fdcdfe81cc Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 26 Jun 2022 11:48:08 -0600 Subject: [PATCH 52/78] citar-embark: Mark additional actions as multi-target The actions are `citar-open`, `citar-open-files`, `citar-open-links`, and `citar-attach-files`. --- citar-embark.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/citar-embark.el b/citar-embark.el index caa498d1..1e5fd965 100644 --- a/citar-embark.el +++ b/citar-embark.el @@ -49,7 +49,8 @@ (citar-citation . citar-embark-citation-map))) ; at-point citations (defvar citar-embark--multitarget-actions - (list #'citar-insert-bibtex #'citar-insert-citation #'citar-insert-reference + (list #'citar-open #'citar-open-files #'citar-attach-files #'citar-open-links + #'citar-insert-bibtex #'citar-insert-citation #'citar-insert-reference #'citar-copy-reference #'citar-insert-keys #'citar-run-default-action)) (defvar citar-embark--target-injection-hooks From f8071fc287af66dd51f40062a5ed99c0355df2aa Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 26 Jun 2022 19:48:48 -0400 Subject: [PATCH 53/78] Set citar-library-file-extensions --- citar.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/citar.el b/citar.el index 730f0764..5e52b86b 100644 --- a/citar.el +++ b/citar.el @@ -115,7 +115,7 @@ :group 'citar :type '(repeat directory)) -(defcustom citar-library-file-extensions nil +(defcustom citar-library-file-extensions '("pdf" "html") "List of file extensions to filter for related files. These are the extensions the `citar-file-open-function' From b7e444843ed0013e1d2e9261fb99c1065938392b Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 27 Jun 2022 18:31:24 -0600 Subject: [PATCH 54/78] Replace cl-seq functions with seq equivalents. --- citar-cache.el | 3 ++- citar.el | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/citar-cache.el b/citar-cache.el index da3e8256..77ee5344 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -27,6 +27,7 @@ (require 'cl-lib)) (require 'parsebib) (require 'citar-format) +(require 'seq) (require 'map) (declare-function citar--get-template "citar") @@ -216,7 +217,7 @@ modified since the last time BIB was updated." ;; CSL-JSOM lets citekey be an arbitrary string. Quote it if... (keyquoted (if (or (string-empty-p citekey) ; ... it's empty, (= ?\" (aref citekey 0)) ; ... starts with ", - (cl-find ?\s citekey)) ; ... or has a space + (seq-contains-p citekey ?\s #'=)) ; ... or has a space (prin1-to-string citekey) citekey)) (prefix (propertize (concat keyquoted (when (cdr preformat) " ")) diff --git a/citar.el b/citar.el index 5e52b86b..85ddccbe 100644 --- a/citar.el +++ b/citar.el @@ -675,7 +675,7 @@ nil." (unless (string-empty-p candidate) (if (= ?\" (aref candidate 0)) (read candidate) - (substring-no-properties candidate 0 (cl-position ?\s candidate))))) + (substring-no-properties candidate 0 (seq-position candidate ?\s #'=))))) (defun citar--key-at-point () "Return bibliography key at point in current buffer, along with its bounds. From 5104ba6fa770812b622ab4e22cbffb19f08377ec Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 27 Jun 2022 18:16:43 -0600 Subject: [PATCH 55/78] Improve file parsing. * `citar-file--parser-default` will now only split file fields by semicolons, not colons. The colon-separated fields seen in #283 are actually triplet-format fields, and are handled by `citar-file--parser-triplet`. * `citar-file--parse-file-field` will now print messages if it fails for any of the following reasons: - The file field exists but is empty. - The file field cannot be parsed; it is non-empty but no parser produces output. - None of the parsed file names actually exist on disk. - None of the parsed files have an extension listed in `citar-library-file-extensions`. * `citar-get-file` no longer filters for `citar-library-file-extensions`. This is now the responsibility of the functions in `citar-get-files-functions`, if appropriate. * `citar--library-file-action`, which does the work for `citar-open-files` and `citar-attach-files`, will only print the "no associated files" warning if none of its input keys had files according to `citar-has-files`. Otherwise, if a key was supposed to have files but turned out not to, the appropriate function in `citar-get-files-functions` should produce a message explaining why. --- citar-file.el | 90 +++++++++++++++++++++++++++-------------- citar.el | 62 ++++++++++++++++++---------- test/citar-file-test.el | 85 ++++++++++++++++++++++++++++++++------ 3 files changed, 173 insertions(+), 64 deletions(-) diff --git a/citar-file.el b/citar-file.el index 1a31471b..aa16e331 100644 --- a/citar-file.el +++ b/citar-file.el @@ -109,11 +109,12 @@ separator that does not otherwise occur in citation keys." ;;;; Parsing file fields (defun citar-file--parser-default (file-field) - "Split FILE-FIELD by both : and ;." - (mapcan (lambda (sepchar) - (mapcar #'string-trim - (citar-file--split-escaped-string file-field sepchar))) - ";:")) + "Split FILE-FIELD by `;'." + (seq-remove + #'string-empty-p + (mapcar + #'string-trim + (citar-file--split-escaped-string file-field ?\;)))) (defun citar-file--parser-triplet (file-field) "Return a list of files from DIRS and a FILE-FIELD formatted as a triplet. @@ -137,25 +138,36 @@ Example: ':/path/to/test.pdf:PDF'." (push escaped filenames)))))) (nreverse filenames))) -(defun citar-file--parse-file-field (file-field dirs) - "Return files listed in FILE-FIELD. +(defun citar-file--parse-file-field (entry dirs &optional citekey) + "Return files found in file field of ENTRY. Relative file names are expanded from the first directory in DIRS -in which they are found; if they are not found in any directory, -they are omitted. Files with absolute paths are included as-is, -even if they don't exist." - (let (filenames) - (dolist (parser citar-file-parser-functions) - (dolist (filename (funcall parser file-field)) - (if (or (null dirs) (file-name-absolute-p filename)) - (push filename filenames) - (when-let ((filename (seq-some - (lambda (dir) - (let ((filepath (expand-file-name filename dir))) - (when (file-exists-p filepath) - filepath))) - dirs))) - (push filename filenames))))) - (nreverse filenames))) +in which they are found. Omit non-existing absolute file names +and relative file names not found in DIRS. On failure, print a +message explaining the cause; CITEKEY is included in this failure +message." + (when-let* ((fieldname citar-file-variable) + (fieldvalue (citar-get-value fieldname entry))) + (if-let ((files (delete-dups (mapcan (lambda (parser) + (funcall parser fieldvalue)) + citar-file-parser-functions)))) + (if-let ((foundfiles (citar-file--find-files-in-dirs files dirs))) + (if (null citar-library-file-extensions) + foundfiles + (or (seq-filter (lambda (file) + (member (file-name-extension file) citar-library-file-extensions)) + foundfiles) + (ignore + (message "No files for `%s' with `citar-library-file-extensions': %S" + citekey foundfiles)))) + (ignore + (message (concat "None of the files for `%s' exist; check `citar-library-paths' and " + "`citar-file-parser-functions': %S") + citekey files))) + (ignore + (if (string-empty-p (string-trim fieldvalue)) + (message "Empty `%s' field: %s" fieldname citekey) + (message "Could not parse `%s' field of `%s'; check `citar-file-parser-functions': %s" + fieldname citekey fieldvalue)))))) (defun citar-file--has-file-field (entries) "Return predicate to test if bibliography entry in ENTRIES has a file field. @@ -176,19 +188,22 @@ Parse and return files given in the bibliography field named by Note: this function is intended to be used in `citar-get-files-functions'. Use `citar-get-files' to get all files associated with KEYS." - (when-let ((filefield citar-file-variable)) - (citar--check-configuration 'citar-library-paths) - (let ((dirs (append citar-library-paths (mapcar #'file-name-directory (citar--bibliography-files))))) - (mapcan (lambda (citekey) - (when-let ((entry (gethash citekey entries))) - (citar-file--parse-file-field (citar-get-value citar-file-variable entry) dirs))) - keys)))) + (when citar-file-variable + (citar--check-configuration 'citar-library-paths 'citar-library-file-extensions + 'citar-file-parser-functions) + (let ((dirs (append citar-library-paths + (mapcar #'file-name-directory (citar--bibliography-files))))) + (mapcan + (lambda (citekey) + (when-let ((entry (gethash citekey entries))) + (citar-file--parse-file-field entry dirs citekey))) + keys)))) ;;;; Scanning library directories (defun citar-file--has-library-files (&optional _entries) "Return predicate testing whether cite key has library files." - (citar--check-configuration 'citar-library-paths) + (citar--check-configuration 'citar-library-paths 'citar-library-file-extensions) (let ((files (citar-file--directory-files citar-library-paths nil citar-library-file-extensions citar-file-additional-files-separator))) @@ -363,5 +378,18 @@ SEPCHAR." (push (buffer-string) strings)) (nreverse strings))) +(defun citar-file--find-files-in-dirs (files dirs) + "Expand file names in FILES in DIRS and keep the ones that exist." + (let (foundfiles) + (dolist (file files) + (if (file-name-absolute-p file) + (when (file-exists-p file) (push (expand-file-name file) foundfiles)) + (when-let ((filepath (seq-some (lambda (dir) + (let ((filepath (expand-file-name file dir))) + (when (file-exists-p filepath) filepath))) + dirs))) + (push filepath foundfiles)))) + (nreverse foundfiles))) + (provide 'citar-file) ;;; citar-file.el ends here diff --git a/citar.el b/citar.el index 85ddccbe..fc87ca02 100644 --- a/citar.el +++ b/citar.el @@ -115,7 +115,7 @@ :group 'citar :type '(repeat directory)) -(defcustom citar-library-file-extensions '("pdf" "html") +(defcustom citar-library-file-extensions nil "List of file extensions to filter for related files. These are the extensions the `citar-file-open-function' @@ -292,6 +292,7 @@ reference has associated notes." :group 'citar :type '(function)) +;; TODO Redundant with `citar-open-note-functions'? (defcustom citar-open-note-function 'citar--open-note "Function to open a new or existing note. @@ -787,11 +788,8 @@ bibliography entries. ENTRIES should also contain any items that are potentially cross-referenced from elements of KEYS. Find files using `citar-get-files-functions'." - (let* ((keys (citar--with-crossref-keys key-or-keys entries)) - (files (mapcan (lambda (fn) (funcall fn keys entries)) citar-get-files-functions))) - (seq-filter (lambda (filename) - (member (file-name-extension filename) citar-library-file-extensions)) - (delete-dups files)))) + (when-let ((keys (citar--with-crossref-keys key-or-keys entries))) + (delete-dups (mapcan (lambda (fn) (funcall fn keys entries)) citar-get-files-functions)))) (cl-defun citar-get-links (key-or-keys &key (entries (citar-get-entries))) @@ -1114,12 +1112,20 @@ With prefix, rebuild the cache before offering candidates." (defun citar--library-file-action (key-or-keys action) "Run ACTION on file associated with KEY-OR-KEYS." - (if-let* ((files (citar-get-files key-or-keys)) - (file (if (null (cdr files)) - (car files) - (citar--select-resource files)))) - (funcall action file) - (message "No associated files for %s" key-or-keys))) + (let ((entries (citar-get-entries))) + (if-let ((files (citar-get-files key-or-keys :entries entries))) + (funcall action (if (null (cdr files)) + (car files) + (citar--select-resource files))) + (ignore + ;; If some key had files according to `citar-has-files', but `citar-get-files' returned nothing, then + ;; don't print the following message. The appropriate function in `citar-get-files-functions' is + ;; responsible for telling the user why it failed, and we want that explanation to appear in the echo + ;; area. + (let ((keys (if (listp key-or-keys) key-or-keys (list key-or-keys))) + (hasfilep (citar-has-files :entries entries))) + (unless (and hasfilep (seq-some hasfilep keys)) + (message "No associated files for %s" key-or-keys))))))) ;;;###autoload (defun citar-open-notes (key) @@ -1318,15 +1324,29 @@ URL." (citar-run-default-action (if (listp keys) keys (list keys))) (user-error "No citation keys found"))) -(defun citar--check-configuration (variable) - "Signal error if VARIABLE has a value of the wrong type. -VARIABLE should be a Citar customization variable." - (pcase variable - ((or 'citar-library-paths 'citar-notes-paths) - (let ((value (symbol-value variable))) - (unless (and (listp value) - (seq-every-p #'stringp value)) - (error "`%S' should be a list of directories: %S" variable `',value)))))) +(defun citar--check-configuration (&rest variables) + "Signal error if any VARIABLES have values of the wrong type. +VARIABLES should be the names of Citar customization variables." + (dolist (variable variables) + (unless (boundp variable) + (error "Unbound variable in citar--check-configuration: %s" variable)) + (let ((value (symbol-value variable))) + (pcase variable + ((or 'citar-library-paths 'citar-notes-paths) + (unless (and (listp value) + (seq-every-p #'stringp value)) + (error "`%s' should be a list of directories: %S" variable `',value))) + ((or 'citar-library-file-extensions) + (unless (and (listp value) + (seq-every-p #'stringp value)) + (error "`%s' should be a list of strings: %S" variable `',value))) + ((or 'citar-has-files-functions 'citar-get-files-functions + 'citar-has-notes-functions 'citar-open-note-functions + 'citar-file-parser-functions) + (unless (and (listp value) (seq-every-p #'functionp value)) + (error "`%s' should be a list of functions: %S" variable `',value))) + (_ + (error "Unknown variable in citar--check-configuration: %s" variable)))))) (provide 'citar) ;;; citar.el ends here diff --git a/test/citar-file-test.el b/test/citar-file-test.el index a2a7a1a7..789fefb0 100644 --- a/test/citar-file-test.el +++ b/test/citar-file-test.el @@ -6,28 +6,30 @@ (require 'ert) (require 'seq) -(require 'citar-file) +(require 'citar) -(ert-deftest citar-format-test--parsing () +(ert-deftest citar-file-test--parser-default () - ;; Test the default parser, which splits strings by both : and ; - (should (equal '("") (delete-dups (citar-file--parser-default " ")))) + (should-not (citar-file--parser-default " ")) (should (equal '("foo") (delete-dups (citar-file--parser-default "foo")))) - (should (equal '("foo" "bar" "foo;bar") (delete-dups (citar-file--parser-default "foo;bar")))) - (should (equal '("foo" "bar" "foo ; bar") (delete-dups (citar-file--parser-default " foo ; bar ")))) - (should (equal '("foo : bar" "foo" "bar") (delete-dups (citar-file--parser-default " foo : bar ")))) - (should (equal '("foo:bar" "baz" "foo" "bar;baz") (delete-dups (citar-file--parser-default "foo:bar;baz")))) + (should (equal '("foo" "bar") (delete-dups (citar-file--parser-default "foo;bar")))) + (should (equal '("foo" "bar") (delete-dups (citar-file--parser-default " foo ; bar ; ")))) + (should (equal '("foo:bar" "baz") (delete-dups (citar-file--parser-default "foo:bar;baz")))) ;; Test escaped delimiters (should (equal '("foo\\;bar") (delete-dups (citar-file--parser-default "foo\\;bar")))) - (should (equal '("foo" "bar\\" "foo;bar\\") (delete-dups (citar-file--parser-default "foo;bar\\")))) - (should (equal '("foo\\;bar" "baz" "foo\\;bar;baz") - (delete-dups (citar-file--parser-default "foo\\;bar;baz")))) + (should (equal '("foo" "bar\\") (delete-dups (citar-file--parser-default "foo;bar\\")))) + (should (equal '("foo\\;bar" "baz") + (delete-dups (citar-file--parser-default "foo\\;bar;baz"))))) + +(ert-deftest citar-file-test--parser-triplet () + + (should-not (citar-file--parser-triplet "foo.pdf")) - ;; Test triplet parser (should (equal '("foo.pdf") (delete-dups (citar-file--parser-triplet ":foo.pdf:PDF")))) (should (equal '("foo.pdf:PDF,:bar.pdf" "foo.pdf" "bar.pdf") (delete-dups (citar-file--parser-triplet ":foo.pdf:PDF,:bar.pdf:PDF")))) + ;; Don't trim spaces in triplet parser since file is delimited by : (should (equal '(" foo.pdf :PDF, : bar.pdf " " foo.pdf " " bar.pdf ") (delete-dups (citar-file--parser-triplet ": foo.pdf :PDF, : bar.pdf :PDF")))) @@ -42,5 +44,64 @@ (should (equal '("C:title.pdf" "C:\\title.pdf") (delete-dups (citar-file--parser-triplet "Title\\: Subtitle:C:\\title.pdf:PDF"))))) +(ert-deftest citar-file-test--parse-file-field () + + (let* ((fieldname "file") + (citekey "foo") + (entry '((file . "foo.pdf"))) + (dirs '("/home/user/library/")) + (citar-file-variable fieldname) + (citar-file-parser-functions (list #'citar-file--parser-default)) + lastmessage) + + (cl-letf (((symbol-function 'message) + (lambda (format-string &rest args) + (setq lastmessage (apply #'format-message format-string args)))) + ((symbol-function 'current-message) + (lambda () + (prog1 lastmessage (setq lastmessage nil)))) + ;; Pretend that all .pdf files under /home/user/library/ exist: + ((symbol-function 'file-exists-p) + (lambda (filename) + (and (equal "pdf" (file-name-extension filename)) + (member (file-name-directory filename) dirs))))) + + (should-not (citar-file--parse-file-field '((file . " ")) dirs citekey)) + (should (string= + (current-message) + (format-message "Empty `%s' field: %s" fieldname citekey))) + + (let ((citar-file-parser-functions nil)) + (should-not (citar-file--parse-file-field entry dirs citekey)) + (should (string= + (current-message) + (format-message + "Could not parse `%s' field of `%s'; check `citar-file-parser-functions': %s" + fieldname citekey (alist-get 'file entry))))) + + (should-not (citar-file--parse-file-field '((file . "foo.html")) dirs citekey)) + (should (string= + (current-message) + (format-message + (concat "None of the files for `%s' exist; check `citar-library-paths' and " + "`citar-file-parser-functions': %S") + citekey '("foo.html")))) + + (let ((citar-library-file-extensions '("html"))) + (should-not (citar-file--parse-file-field entry dirs citekey)) + (should (string= + (current-message) + (format-message + "No files for `%s' with `citar-library-file-extensions': %S" + citekey '("/home/user/library/foo.pdf"))))) + + (let ((citar-library-file-extensions nil)) + (should (equal (citar-file--parse-file-field entry dirs citekey) + '("/home/user/library/foo.pdf")))) + + (let ((citar-library-file-extensions '("pdf" "html"))) + (should (equal (citar-file--parse-file-field entry dirs citekey) + '("/home/user/library/foo.pdf"))))))) + (provide 'citar-file-test) ;;; citar-file-test.el ends here From 69ca063dc253275290aa25cb13b228b306873d1b Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Mon, 27 Jun 2022 19:19:11 -0600 Subject: [PATCH 56/78] Ignore CI errors on emacs 29 and don't fail on package-lint warnings --- .github/workflows/check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4aaf39ec..15b9e2ff 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,6 +17,7 @@ on: jobs: check: runs-on: ubuntu-latest + continue-on-error: ${{ matrix.emacs_version == 'snapshot' }} strategy: fail-fast: false matrix: @@ -28,6 +29,9 @@ jobs: - compile - test - lint + exclude: + - emacs_version: snapshot + action: compile steps: - name: Set up Emacs ${{matrix.emacs_version}} uses: purcell/setup-emacs@master @@ -63,5 +67,6 @@ jobs: eldev --color lint re - name: Lint package metadata if: ${{ matrix.action == 'lint' }} + continue-on-error: true run: | eldev --color lint package From 1ef0160af46fa2eee71e209eba494d0262e3ffb0 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 28 Jun 2022 09:26:02 -0400 Subject: [PATCH 57/78] Revert removal of entry from create-note-function --- citar-org.el | 5 ++--- citar.el | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/citar-org.el b/citar-org.el index 3b095d61..3e621bfb 100644 --- a/citar-org.el +++ b/citar-org.el @@ -289,10 +289,9 @@ With optional argument FORCE, force the creation of a new ID." (ignore-errors (org-roam-ref-add (concat "@" key))))) ;;;###autoload -(defun citar-org-format-note-default (key) - "Format a note from KEY." +(defun citar-org-format-note-default (key entry) + "Format a note from KEY and ENTRY." (let* ((template (citar--get-template 'note)) - (entry (citar-get-entry key)) (note-meta (when template (citar-format--entry template entry))) (filepath (expand-file-name diff --git a/citar.el b/citar.el index fc87ca02..6d90f56d 100644 --- a/citar.el +++ b/citar.el @@ -1139,10 +1139,11 @@ With prefix, rebuild the cache before offering candidates." (defun citar--open-notes (key) "Open note(s) associated with KEY." - (or (seq-some - (lambda (opener) - (funcall opener key)) citar-open-note-functions) - (funcall citar-create-note-function key))) + (let ((entry (citar-get-entry key))) + (or (seq-some + (lambda (opener) + (funcall opener key)) citar-open-note-functions) + (funcall citar-create-note-function key entry)))) ;;;###autoload (defun citar-open-entry (key) From 66f7b5800737ee4baa0a8e28495c59d8478a08c3 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Thu, 30 Jun 2022 08:42:21 -0400 Subject: [PATCH 58/78] Add back entry to function call --- citar.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/citar.el b/citar.el index 6d90f56d..6788efd2 100644 --- a/citar.el +++ b/citar.el @@ -1142,7 +1142,7 @@ With prefix, rebuild the cache before offering candidates." (let ((entry (citar-get-entry key))) (or (seq-some (lambda (opener) - (funcall opener key)) citar-open-note-functions) + (funcall opener key entry)) citar-open-note-functions) (funcall citar-create-note-function key entry)))) ;;;###autoload From 188f7cfca5658dc55b58987225949665d40620e0 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 1 Jul 2022 13:21:49 -0400 Subject: [PATCH 59/78] Add citar-cache-refresh --- citar-cache.el | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/citar-cache.el b/citar-cache.el index 77ee5344..a473479f 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -32,6 +32,7 @@ (declare-function citar--get-template "citar") (declare-function citar--fields-to-parse "citar") +(declare-function citar--bibliography-files "citar") (defvar citar-ellipsis) @@ -181,6 +182,15 @@ needed by any other buffer." ;;; Updating bibliographies +(defun citar-cache-refresh (&optional force) + "Refresh the bibliography cache. + +Unless FORCE is non-nil, the cached bib files are only reread if +modified since the last time they were updated." + (interactive) + (dolist (file (citar--bibliography-files)) + (let ((bib (citar-cache--get-bibliography file))) + (citar-cache--update-bibliography bib force)))) (defun citar-cache--update-bibliography (bib &optional force) "Update the bibliography BIB from the original file. From 40bc6bcc85ffd4fde835e7b1cf855d38070d6aad Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Fri, 1 Jul 2022 18:28:46 -0400 Subject: [PATCH 60/78] Refactor citar--select-resource Change the function to use 'cl-defun', use keyword args, and add an option for 'note-nodes', so accomodate cases like org-roam. --- citar-file.el | 16 ++++-- citar.el | 143 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 115 insertions(+), 44 deletions(-) diff --git a/citar-file.el b/citar-file.el index aa16e331..41b86b86 100644 --- a/citar-file.el +++ b/citar-file.el @@ -27,6 +27,7 @@ (require 'cl-lib) (require 'subr-x)) (require 'seq) +(require 'map) ;;; pre-1.0 API cleanup @@ -321,11 +322,15 @@ need to scan the contents of DIRS in this case." ;;;; Note files +(defun citar-file--get-notes-hash (&optional keys) + "Return hash-table with KEYS with file notes." + (citar-file--directory-files + citar-notes-paths keys citar-file-note-extensions + citar-file-additional-files-separator)) + (defun citar-file-has-notes (&optional _entries) "Return predicate testing whether cite key has associated notes." - (let ((files (citar-file--directory-files - citar-notes-paths nil citar-file-note-extensions - citar-file-additional-files-separator))) + (let ((files (citar-file--get-notes-hash))) (lambda (key) (gethash key files)))) @@ -342,6 +347,11 @@ need to scan the contents of DIRS in this case." (error "You must set 'citar-notes-paths'") (funcall citar-create-note-function key entry file)))) +(defun citar-file--get-note-files (keys) + "Return list of notes associated with KEYS." + (let ((notehash (citar-file--get-notes-hash keys))) + (flatten-list (map-values notehash)))) + (defun citar-file--get-note-filename (key dirs extensions) "Return existing or new filename for KEY in DIRS with extension in EXTENSIONS. diff --git a/citar.el b/citar.el index 6788efd2..e710c8ce 100644 --- a/citar.el +++ b/citar.el @@ -292,6 +292,31 @@ reference has associated notes." :group 'citar :type '(function)) +(defvar citar-notes-config-file + `(:name "Notes" + :category file + :key-predicate ,#'citar-file-has-notes + :action ,#'citar-file--open-note + :items ,#'citar-file--get-note-files) + "Default file-per-note configuration.") + +(defvar citar-notes-config citar-notes-config-file +;; FIX doesn't work as defcustom + "Configuration plist for notes, with following properties: + +:name the group display name + +:category either 'file' or 'node-note' + +:key-predicate function to test for keys with notes + +:action function to open a given note candidate + +:items function to return candidate strings for keys + +:annotate annotation function") + + ;; TODO Redundant with `citar-open-note-functions'? (defcustom citar-open-note-function 'citar--open-note @@ -596,37 +621,76 @@ HISTORY is the `completing-read' history argument." (equal item ""))))) (hash-table-keys selected-hash))) -(defun citar--select-resource (files &optional links) - "Select resource from a list of FILES, and optionally LINKS." - (let* ((files (mapcar - (lambda (file) - (propertize (abbreviate-file-name file) 'multi-category `(file . ,file))) - files)) - (links (mapcar - (lambda (link) - (propertize link 'multi-category `(url . ,link))) - links)) - (resources (delete-dups (append files links)))) +(cl-defun citar--get-resource-candidates (keys &key files notes links) + "Return related resource candidates for KEYS. + +Optionally constrain to FILES, NOTES, and/or LINKS." + (let* ((filesource + (when files + (cons 'file + (let ((citar-library-file-extensions nil)) + (citar-get-files keys))))) + (linksource + (when links + (cons 'url (citar-get-links keys)))) + (notesource + (when notes + (let* ((cat (plist-get citar-notes-config :category)) + (items (plist-get citar-notes-config :items)) + (items (if (functionp items) (funcall items keys) items))) + (cons cat items)))) + (sources (list filesource linksource notesource)) + (candidates (list)) + ;; Only use multi-category if we need to. + (multicat (< 1 (length (delete nil sources))))) + (progn + (dolist (source sources) + (let ((cat (car source))) + (dolist (cand (cdr source)) + (push + (if multicat + (propertize cand 'multi-category (cons cat cand)) cand) + candidates)))) + candidates))) + +(defun citar--multi-annotate (cand) + "Annotate candidate CAND with `consult--multi' type." + ;; Adapted from 'consult' + (let* ((nodecat (car (get-text-property 0 'multi-category cand))) + (notecat (plist-get citar-notes-config :category)) + (annotate (plist-get citar-notes-config :annotate)) + (ann (when (and annotate (string= nodecat notecat)) + (funcall annotate (cdr (get-text-property 0 'multi-category cand)))))) + ann)) + +(cl-defun citar--select-resource (keys &optional &key files notes links) + ;; FIX the arg list above is not smart + "Select related FILES, NOTES, or LINKS resource for KEYS." + (if-let ((resources + (citar--get-resource-candidates + keys :files files :notes notes :links links))) (completing-read "Select resource: " (lambda (string predicate action) + ;; REVIEW how to hook in annotation functions here by category? (if (eq action 'metadata) `(metadata (group-function . citar--select-group-related-resources) + (annotation-function . citar--multi-annotate) (category . multi-category)) (complete-with-action action resources string predicate)))))) (defun citar--select-group-related-resources (resource transform) "Group RESOURCE by type or TRANSFORM." - (let ((extension (file-name-extension resource))) - (if transform - (if (file-regular-p resource) - (file-name-nondirectory resource) - resource) - (cond - ((member extension citar-file-note-extensions) "Notes") - ((string-prefix-p "http" resource 'ignore-case) "Links") - (t "Library Files"))))) + (if transform + (if (file-regular-p resource) + (file-name-nondirectory resource) + resource) + (let ((cat (car (get-text-property 0 'multi-category resource)))) + (pcase cat + ('file "Library Files") ; FIX this won't work for file notes IUC + ('url "Links") + (_ (plist-get citar-notes-config :name)))))) (cl-defun citar--format-candidates (&key (bibs (citar--bibliographies)) (entries (citar-cache--entries bibs))) @@ -777,9 +841,12 @@ The value is transformed using `citar-display-transform-functions'" ;; Make sure we always return a string, even if empty. (or (cdr fieldvalue) "")))) - ;;;; File, notes, and links +(defun citar-get-notes (keys) + "Return list of notes associated with KEYS." + (funcall (plist-get citar-notes-config :items) keys)) + (cl-defun citar-get-files (key-or-keys &key (entries (citar-get-entries))) "Return list of files associated with KEY-OR-KEYS in ENTRIES. @@ -1062,16 +1129,10 @@ are potentially cross-referenced from elements of KEYS." ;;;###autoload (defun citar-open (keys) "Open related resources (links or files) for KEYS." - (interactive (list - (list (citar-select-ref)))) - (let* ((embark-default-action-overrides - '((multi-category . citar--open-multi) - (file . citar-file-open) - (url . browse-url))) - (files (let ((citar-library-file-extensions nil)) - (citar-get-files keys))) - (links (citar-get-links keys)) - (resource-candidates (delete-dups (append files (remq nil links))))) + (interactive (list (citar-select-refs))) + (let ((resource-candidates + (citar--get-resource-candidates + keys :files t :notes t :links t))) (cond ((eq nil resource-candidates) (error "No associated resources")) @@ -1079,7 +1140,7 @@ are potentially cross-referenced from elements of KEYS." (eq 1 (length resource-candidates))) (citar--open-multi (car resource-candidates))) (t (citar--open-multi - (citar--select-resource files links)))))) + (citar--select-resource keys :files t :notes t :links t)))))) (defun citar--open-multi (selection) "Act appropriately on SELECTION when type is `multi-category'. @@ -1116,7 +1177,9 @@ With prefix, rebuild the cache before offering candidates." (if-let ((files (citar-get-files key-or-keys :entries entries))) (funcall action (if (null (cdr files)) (car files) - (citar--select-resource files))) + ;; REVIEW this function will return files for keys + ;; also, candidates are mult-category, even though only one + (citar--select-resource key-or-keys :files t))) (ignore ;; If some key had files according to `citar-has-files', but `citar-get-files' returned nothing, then ;; don't print the following message. The appropriate function in `citar-get-files-functions' is @@ -1193,15 +1256,13 @@ directory as current buffer." (citar--insert-bibtex key))))) ;;;###autoload -(defun citar-open-links (key-or-keys) - "Open URL or DOI link associated with KEY-OR-KEYS in a browser." - (interactive (list (citar-select-ref))) - (if-let* ((links (citar-get-links key-or-keys)) - (link (if (null (cdr links)) - (car links) - (citar--select-resource nil links)))) +(defun citar-open-links (keys) + "Open URL or DOI link associated with KEYS in a browser." + (interactive (list (citar-select-refs))) + ;; REVIEW this works, but should check for nil on select-resource + (if-let ((link (citar--select-resource keys :links t))) (browse-url link) - (message "No link found for %s" key-or-keys))) + (message "No link found for %s" keys))) ;;;###autoload (defun citar-insert-citation (keys &optional arg) From e6f0b064baa46baeaad5a24a4fdc213c7331b911 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sat, 2 Jul 2022 19:50:24 -0400 Subject: [PATCH 61/78] Change default note category to citar-note-file Also add the marginalia file annotator. --- citar.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/citar.el b/citar.el index e710c8ce..8ab467b3 100644 --- a/citar.el +++ b/citar.el @@ -79,6 +79,7 @@ ;;; Declare variables and functions for byte compiler (defvar embark-default-action-overrides) +(declare-function marginalia-annotate-file "ext:marginalia") ;;; Variables @@ -294,9 +295,10 @@ reference has associated notes." (defvar citar-notes-config-file `(:name "Notes" - :category file + :category citar-note-file :key-predicate ,#'citar-file-has-notes :action ,#'citar-file--open-note + :annotate ,#'marginalia-annotate-file :items ,#'citar-file--get-note-files) "Default file-per-note configuration.") From 3d0299f3d6b66b5ed4925e7151e04a05eac0b4b4 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sat, 2 Jul 2022 19:55:59 -0400 Subject: [PATCH 62/78] Add new notes defcustoms --- citar.el | 55 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/citar.el b/citar.el index 8ab467b3..85ac5086 100644 --- a/citar.el +++ b/citar.el @@ -295,13 +295,41 @@ reference has associated notes." (defvar citar-notes-config-file `(:name "Notes" - :category citar-note-file + :category file :key-predicate ,#'citar-file-has-notes :action ,#'citar-file--open-note - :annotate ,#'marginalia-annotate-file +; :annotate ,#'marginalia-annotate-file :items ,#'citar-file--get-note-files) "Default file-per-note configuration.") +;; TODO hook these up, and remove other variables + +(defcustom citar-notes-sources + '((citar-file . citar-notes-config-file)) + "The alist of notes backends available for configuration. + +The format of the cons should be (NAME . PLIST), where the +plist has the following properties: + + :name the group display name + + :category the completion category + + :key-predicate function to test for keys with notes + + :action function to open a given note candidate + + :items function to return candidate strings for keys + + :annotate annotation function (optional)" + :group 'citar + :type '(alist :key-type symbol :value-type plist)) + +(defcustom citar-notes-source 'citar-file + "The notes backend." + :group 'citar + :type 'symbol) + (defvar citar-notes-config citar-notes-config-file ;; FIX doesn't work as defcustom "Configuration plist for notes, with following properties: @@ -623,6 +651,10 @@ HISTORY is the `completing-read' history argument." (equal item ""))))) (hash-table-keys selected-hash))) +(defun citar--add-notep-prop (candidate) + "Add a note resource CANDIDATE with 'notep t'." + (propertize candidate 'notep t)) + (cl-defun citar--get-resource-candidates (keys &key files notes links) "Return related resource candidates for KEYS. @@ -639,12 +671,13 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (when notes (let* ((cat (plist-get citar-notes-config :category)) (items (plist-get citar-notes-config :items)) - (items (if (functionp items) (funcall items keys) items))) + (items (if (functionp items) (funcall items keys) items)) + (items (mapcar #'citar--add-notep-prop items))) (cons cat items)))) (sources (list filesource linksource notesource)) (candidates (list)) - ;; Only use multi-category if we need to. - (multicat (< 1 (length (delete nil sources))))) + ;; REVIEW initially I deleted nil sources, but I think that's overkill? + (multicat (< 1 (length sources)))) (progn (dolist (source sources) (let ((cat (car source))) @@ -688,11 +721,13 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (if (file-regular-p resource) (file-name-nondirectory resource) resource) - (let ((cat (car (get-text-property 0 'multi-category resource)))) - (pcase cat - ('file "Library Files") ; FIX this won't work for file notes IUC - ('url "Links") - (_ (plist-get citar-notes-config :name)))))) + (let ((cat (car (get-text-property 0 'multi-category resource))) + (notep (get-text-property 0 'notep resource))) + ;; If note, assign to note group; otherwise use completion category. + (if notep (plist-get citar-notes-config :name) + (pcase cat + ('file "Library Files") + ('url "Links")))))) (cl-defun citar--format-candidates (&key (bibs (citar--bibliographies)) (entries (citar-cache--entries bibs))) From 2e799f739ef4c9a3edde38662c1f5f399316e377 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 09:46:43 -0400 Subject: [PATCH 63/78] Modify embark config --- README.org | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.org b/README.org index 3e3f580b..b1e81f83 100644 --- a/README.org +++ b/README.org @@ -17,8 +17,6 @@ :CUSTOM_ID: features :END: -Note: this package was formerly called "bibtex-actions." - This package provides a completing-read front-end to browse and act on BibTeX, BibLaTeX, and CSL JSON bibliographic data, and LaTeX, markdown, and org-cite editing support. When used with vertico, embark, and marginalia, it provides similar functionality to helm-bibtex and ivy-bibtex: quick filtering and selecting of bibliographic entries from the minibuffer, and the option to run different commands against them. @@ -50,6 +48,8 @@ In addition, the following packages are strongly recommended for the best experi 3. [[https://github.com/oantolin/embark][Embark]] (contextual actions) 4. [[https://github.com/minad/marginalia][Marginalia]] (annotations, and also candidate classification for Embark) +We also recommend Emacs 28, as this package relies on two of its features, that greatly enhance the UI. + ** Configuration :PROPERTIES: :CUSTOM_ID: configuration @@ -73,15 +73,14 @@ This is the minimal configuration, and will work with any completing-read compli *** Embark -The =citar-embark= package adds contextual access actions in the minibuffer and at-point. +The =citar-embark= package adds contextual access actions in the minibuffer and at-point via the ~citar-embark-mode~ minor mode`. When using Embark, the Citar actions are generic, and work the same across org, markdown, and latex modes. #+BEGIN_SRC emacs-lisp (use-package citar-embark - :bind (("M-;" . embark-act) - ("M-," . embark-dwim)) - :custom - (citar-embark 1)) + :after citar embark + :no-require + :config (citar-embark-mode)) #+END_SRC *** Org-Cite From 399bac83f9f4477608eef614f1b23704bb10d3f6 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 09:48:03 -0400 Subject: [PATCH 64/78] Temporarily remove citar-filenotify.el --- README.org | 41 +-------- citar-filenotify.el | 202 -------------------------------------------- 2 files changed, 3 insertions(+), 240 deletions(-) delete mode 100644 citar-filenotify.el diff --git a/README.org b/README.org index b1e81f83..b294f169 100644 --- a/README.org +++ b/README.org @@ -223,45 +223,10 @@ You can save this history across sessions by adding =citar-history= to =savehist :CUSTOM_ID: refreshing-the-library-display :END: -=citar= uses two caches to speed up library display; one for the global bibliography, and another for local files specific to a buffer. -This is great for performance, but means the data can become stale if you modify it. +=citar= uses a cache to speed up library display. +If a bib file changes, the cache should automatically update the next time you run a citar command. -The =citar-refresh= command will reload the caches, and you can call this manually. -You can also call any of the =citar= commands with a prefix argument: =C-u M-x citar-insert-key=. - -Although not default, =citar= also provides convenience functions for auto-refreshing cache when bib files change using filenotify. -The simplest use of this functionality is - -#+BEGIN_SRC emacs-lisp -(citar-filenotify-setup '(LaTeX-mode-hook org-mode-hook)) -#+END_SRC - -This will add watches for the global bib files and in addition add a hook to =LaTeX-mode-hook= and =org-mode-hook= to add watches for local bibliographic files. -By default this will invalidate the cache if a bib file changes. If the bib files change rarely, a more suitable option is to refresh the cache. -This can be achieved by - -#+BEGIN_SRC emacs-lisp -(setq citar-filenotify-callback 'refresh-cache) -#+END_SRC - -The behavior can be tweaked more thoroughly by setting ~citar-filenotify-callback~ to a function. -See its documentation for details. -Watches can be also placed on additional files. -This is controlled by the variable ~citar-filenotify-files~. - -Another option to make the completion interface more seamless is to add a hook which generates the cache after a buffer is opened. -This can be done when emacs has been idle (half a second in the example below) with something like this: - -#+BEGIN_SRC emacs-lisp -(defun gen-bib-cache-idle () - "Generate bib item caches with idle timer" - (run-with-idle-timer 0.5 nil #'citar-refresh)) - -(add-hook 'LaTeX-mode-hook #'gen-bib-cache-idle) -(add-hook 'org-mode-hook #'gen-bib-cache-idle) -#+END_SRC - -For additional configuration options on this, see [[https://github.com/bdarcus/citar/wiki/Configuration#automating-path-watches][the wiki]]. +The =citar-cache-refresh= command will reload the caches. ** Notes diff --git a/citar-filenotify.el b/citar-filenotify.el deleted file mode 100644 index d2ffc431..00000000 --- a/citar-filenotify.el +++ /dev/null @@ -1,202 +0,0 @@ -;;; citar-filenotify.el --- Filenotify functions for citar -*- lexical-binding: t; -*- -;; -;; Copyright (C) 2021 Bruce D'Arcus -;; -;; This file is not part of GNU Emacs. -;; -;; This program is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . -;; -;;; Commentary: -;; -;; A companion to the citar for auto-invalidation and auto-refreshing -;; of cache when a bib file or a related file such notes directory or library -;; changes. Uses filenotify api to acheive this. -;; -;;; Code: - -(require 'filenotify) -(require 'files) -(require 'citar) - -(declare-function citar-refresh "citar") -(declare-function citar--local-files-to-cache "citar") -(declare-function citar-file--normalize-paths "citar-file") -(declare-function reftex-access-scan-info "ext:reftex") -(declare-function file-notify-add-watch "ext:file-notify") -(declare-function file-notify-rm-watch "ext:file-notify") - -;;;; Variables - -(defcustom citar-filenotify-callback 'invalidate-cache - "The callback that is run when the bibliography related files change. - -Its value can be either `invalidate-cache', `refresh-cache' or else a function. - -The function takes two arguments. - -The first is the scope, which is `global' when the changed file -is in `citar-filenotify-files' and `local' otherwise. - -The second is the change that occured. This is the argument that -the callback of `file-notify-add-watch' accepts, and is optional. -The callback is called without it when -`citar-filenotify-refresh' is run" - :group 'citar - :type '(choice (const invalidate-cache) - (const refresh-cache) - function)) - -(defcustom citar-filenotify-files '(bibliography) - "The files to watch using filenotify." - :group 'citar - :type '(repeat (choice (const bibliogrpahy) - (const library) - (const notes) - string))) - - -(defvar-local citar-filenotify--local-watches 'uninitialized) -(defvar citar-filenotify--global-watches nil) - -;;; Filenotify functions - -(defun citar-filenotify--invalidate-cache (&optional scope) - "Invalidate local or global caches according to SCOPE. -If it is other than `global' or `local' invalidate both." - (unless (eq 'local scope) - (setq citar--candidates-cache 'uninitialized)) - (unless (eq 'global scope) - (setq citar--local-candidates-cache 'uninitialized))) - -(defun citar-filenotify--make-default-callback (func scope &optional change) - "The callback FUNC by SCOPE used to update cache for default options. - -CHANGE refers to the notify argument." - (pcase (cadr change) - ((or 'nil 'changed) (funcall func scope)) - ((or 'created 'deleted 'renamed) - (if (member - (nth 2 change) - (seq-concatenate 'list - (citar-file--normalize-paths citar-bibliography) - (citar--local-files-to-cache))) - (citar-filenotify-refresh scope) - (funcall func scope))))) - -(defun citar-filenotify--callback (scope &optional change) - "A by SCOPE callback according to `citar-filenotify-callback'. - -This callback can be passed to the `file-notify-add-watch'. - -CHANGE refers to the filenotify argument." - (pcase citar-filenotify-callback - ('invalidate-cache (citar-filenotify--make-default-callback - #'citar-filenotify--invalidate-cache scope change)) - ('refresh-cache (citar-filenotify--make-default-callback - (lambda (x) (citar-refresh nil x)) scope change)) - (_ (funcall citar-filenotify-callback scope change)))) - -(defun citar-filenotify--add-local-watches () - "Add watches for the files that contribute to the local cache." - (let ((buffer (buffer-name))) - (setq citar-filenotify--local-watches - (seq-map - (lambda (bibfile) - (file-notify-add-watch - bibfile '(change) - (lambda (x) - (with-current-buffer buffer - (citar-filenotify--callback 'local x))))) - (citar--local-files-to-cache))))) - -(defun citar-filenotify-rm-local-watches () - "Delete the filenotify watches for the local bib files." - (mapc #'file-notify-rm-watch citar-filenotify--local-watches) - (setq citar-filenotify--local-watches 'uninitialized)) - -(defun citar-filenotify-local-watches () - "Hook to add and remove watches on local bib files. - -The watches are added only if `citar--local-watches' has the -default value `uninitialized'. This is to ensure that duplicate -watches aren't added. This means a mode hook containing this -function can run several times without adding duplicate watches." - (when (eq 'uninitialized citar-filenotify--local-watches) - (citar-filenotify--add-local-watches)) - (add-hook 'kill-buffer-hook #'citar-filenotify-rm-local-watches nil t)) - -(defun citar-filenotify--files () - "Get the list of files to watch from `citar-filenotify-files'." - (seq-mapcat (lambda (x) - (citar-file--normalize-paths - (pcase x - ('bibliography citar-bibliography) - ('library citar-library-paths) - ('notes citar-notes-paths) - (_ x)))) - citar-filenotify-files)) - -(defun citar-filenotify-global-watches () - "Add watches on the global files in `citar-filenotify-files'. - -Unlike `citar-filenotify-local-watches' these -watches have to be removed manually. To remove them call -`citar-rm-global-watches'" - (setq citar-filenotify--global-watches - (seq-map - (lambda (bibfile) - (file-notify-add-watch - bibfile '(change) - (lambda (x) - (citar-filenotify--callback 'global x)))) - (citar-filenotify--files)))) - -(defun citar-filenotify-rm-global-watches () - "Remove the watches on global bib files." - (interactive) - (mapc #'file-notify-rm-watch citar-filenotify--global-watches) - (setq citar-filenotify--global-watches nil)) - -(defun citar-filenotify-refresh (&optional scope) - "Refresh the watches by SCOPE on the bib files. - -This function only needs to be called if a bib file has been added or removed." - (interactive) - (unless (eq 'global scope) - (seq-map #'file-notify-rm-watch citar-filenotify--local-watches) - (reftex-access-scan-info t) - (citar-filenotify--add-local-watches) - (citar-filenotify--callback 'local)) - (unless (eq 'local scope) - (citar-filenotify-rm-global-watches) - (citar-filenotify-global-watches) - (citar-filenotify--callback 'global))) - -;;;; Interactive filenoify commands - -;;;###autoload -(defun citar-filenotify-setup (mode-hooks) - "Setup filenotify watches for local and global bibliography related files. - -This functions adds watches to the files in -`citar-filenotify-files' and adds a hook to the -major mode hooks in 'MODE-HOOKS' which adds watches for the -local bib files. These local watches are removed when the buffer -closes." - (citar-filenotify-global-watches) - (dolist (mode mode-hooks) - (add-hook mode #'citar-filenotify-local-watches))) - -(provide 'citar-filenotify) -;;; citar-filenotify.el ends here From 1cb5936676443e630a7c56f9e68181bd95ebe1aa Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 10:23:40 -0400 Subject: [PATCH 65/78] Fix the citar-note-sources defcustom syntax --- citar.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/citar.el b/citar.el index 85ac5086..b0b287be 100644 --- a/citar.el +++ b/citar.el @@ -305,7 +305,7 @@ reference has associated notes." ;; TODO hook these up, and remove other variables (defcustom citar-notes-sources - '((citar-file . citar-notes-config-file)) + `((citar-file . ,citar-notes-config-file)) "The alist of notes backends available for configuration. The format of the cons should be (NAME . PLIST), where the From df1b24e5a5bcbc8d107e545f009c6aa7c1bf3a89 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 10:59:47 -0400 Subject: [PATCH 66/78] Fix note config, add access function --- citar.el | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/citar.el b/citar.el index b0b287be..d898e644 100644 --- a/citar.el +++ b/citar.el @@ -298,14 +298,18 @@ reference has associated notes." :category file :key-predicate ,#'citar-file-has-notes :action ,#'citar-file--open-note -; :annotate ,#'marginalia-annotate-file :items ,#'citar-file--get-note-files) "Default file-per-note configuration.") ;; TODO hook these up, and remove other variables (defcustom citar-notes-sources - `((citar-file . ,citar-notes-config-file)) + `((citar-file . + ,(list :name "Notes" + :category 'file + :key-predicate #'citar-file-has-notes + :action #'citar-file--open-note + :items #'citar-file--get-note-files))) "The alist of notes backends available for configuration. The format of the cons should be (NAME . PLIST), where the @@ -651,6 +655,7 @@ HISTORY is the `completing-read' history argument." (equal item ""))))) (hash-table-keys selected-hash))) + (defun citar--add-notep-prop (candidate) "Add a note resource CANDIDATE with 'notep t'." (propertize candidate 'notep t)) @@ -669,8 +674,8 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (cons 'url (citar-get-links keys)))) (notesource (when notes - (let* ((cat (plist-get citar-notes-config :category)) - (items (plist-get citar-notes-config :items)) + (let* ((cat (citar--get-notes-config-property :category)) + (items (citar--get-notes-config-property :items)) (items (if (functionp items) (funcall items keys) items)) (items (mapcar #'citar--add-notep-prop items))) (cons cat items)))) @@ -692,8 +697,8 @@ Optionally constrain to FILES, NOTES, and/or LINKS." "Annotate candidate CAND with `consult--multi' type." ;; Adapted from 'consult' (let* ((nodecat (car (get-text-property 0 'multi-category cand))) - (notecat (plist-get citar-notes-config :category)) - (annotate (plist-get citar-notes-config :annotate)) + (notecat (citar--get-notes-config-property :category)) + (annotate (citar--get-notes-config-property :annotate)) (ann (when (and annotate (string= nodecat notecat)) (funcall annotate (cdr (get-text-property 0 'multi-category cand)))))) ann)) @@ -724,7 +729,7 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (let ((cat (car (get-text-property 0 'multi-category resource))) (notep (get-text-property 0 'notep resource))) ;; If note, assign to note group; otherwise use completion category. - (if notep (plist-get citar-notes-config :name) + (if notep (citar--get-notes-config-property :name) (pcase cat ('file "Library Files") ('url "Links")))))) @@ -880,9 +885,14 @@ The value is transformed using `citar-display-transform-functions'" ;;;; File, notes, and links +(defun citar--get-notes-config-property (property) + "Return PROPERTY value for configured notes backend." + (plist-get + (alist-get citar-notes-source citar-notes-sources) property)) + (defun citar-get-notes (keys) "Return list of notes associated with KEYS." - (funcall (plist-get citar-notes-config :items) keys)) + (funcall (citar--get-notes-config-property :items) keys)) (cl-defun citar-get-files (key-or-keys &key (entries (citar-get-entries))) "Return list of files associated with KEY-OR-KEYS in ENTRIES. From 412f97903b7ced7b7dd8b4ecceae2dfddaac4781 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 11:13:52 -0400 Subject: [PATCH 67/78] Remove legacy notes defcustoms --- citar.el | 69 -------------------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/citar.el b/citar.el index d898e644..17d1becd 100644 --- a/citar.el +++ b/citar.el @@ -277,32 +277,6 @@ If nil, single resources will open without prompting." :group 'citar :type '(repeat function)) -(defcustom citar-open-note-functions - '(citar-file--open-note) - "List of functions to open a note." - ;; REVIEW change to key only arg? - :group 'citar - :type '(function)) - -(defcustom citar-has-notes-functions '(citar-file-has-notes) - "Functions used for displaying note indicators. - -Such functions must take KEY and return non-nil when the -reference has associated notes." - ;; REVIEW change to key only arg? - :group 'citar - :type '(function)) - -(defvar citar-notes-config-file - `(:name "Notes" - :category file - :key-predicate ,#'citar-file-has-notes - :action ,#'citar-file--open-note - :items ,#'citar-file--get-note-files) - "Default file-per-note configuration.") - -;; TODO hook these up, and remove other variables - (defcustom citar-notes-sources `((citar-file . ,(list :name "Notes" @@ -334,49 +308,6 @@ plist has the following properties: :group 'citar :type 'symbol) -(defvar citar-notes-config citar-notes-config-file -;; FIX doesn't work as defcustom - "Configuration plist for notes, with following properties: - -:name the group display name - -:category either 'file' or 'node-note' - -:key-predicate function to test for keys with notes - -:action function to open a given note candidate - -:items function to return candidate strings for keys - -:annotate annotation function") - - -;; TODO Redundant with `citar-open-note-functions'? -(defcustom citar-open-note-function - 'citar--open-note - "Function to open a new or existing note. - -A note function must take two arguments: - -KEY: a string to represent the citekey -ENTRY: an alist with the structured data (title, author, etc.)" - ;; REVIEW change to key only arg? - :group 'citar - :type 'function) - -(defcustom citar-create-note-function - 'citar-org-format-note-default - "Function to create a new note. - -A note function must take three arguments: - -KEY: a string to represent the citekey -ENTRY: an alist with the structured data (title, author, etc.) -FILEPATH: the file name." - ;; REVIEW change to key only arg? - :group 'citar - :type 'function) - ;; TODO Move this to `citar-org', since it's only used there? ;; Otherwise it seems to overlap with `citar-default-action' (defcustom citar-at-point-function #'citar-dwim From cc22f010778212502ff563c8c1e2a6dc705894f5 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 12:14:04 -0400 Subject: [PATCH 68/78] Update citar-open-notes This still needs some review and possible refinement. For example, if more than one, maybe use select-resource? --- citar.el | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/citar.el b/citar.el index 17d1becd..132acd82 100644 --- a/citar.el +++ b/citar.el @@ -1169,22 +1169,17 @@ With prefix, rebuild the cache before offering candidates." (message "No associated files for %s" key-or-keys))))))) ;;;###autoload -(defun citar-open-notes (key) - "Open notes associated with the KEY." - ;; REVIEW KEY, or KEYS - (interactive (list (citar-select-ref))) - (let ((embark-default-action-overrides '((file . find-file)))) - (if (listp citar-open-note-functions) - (citar--open-notes key) - (error "Please change the value of 'citar-open-note-functions' to a list")))) +(defun citar-open-notes (keys) + "Open notes associated with the KEYS." + (interactive (list (citar-select-refs))) + (dolist (key keys) + (funcall 'citar--open-notes key))) (defun citar--open-notes (key) "Open note(s) associated with KEY." (let ((entry (citar-get-entry key))) - (or (seq-some - (lambda (opener) - (funcall opener key entry)) citar-open-note-functions) - (funcall citar-create-note-function key entry)))) + (funcall + (citar--get-notes-config-property :action) key entry))) ;;;###autoload (defun citar-open-entry (key) From 00b3279dfa5b6e4fba9782a141f30d5aa7aa8995 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 15:49:00 -0400 Subject: [PATCH 69/78] Move citar-open-links --- citar.el | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/citar.el b/citar.el index 132acd82..58c33844 100644 --- a/citar.el +++ b/citar.el @@ -1181,6 +1181,15 @@ With prefix, rebuild the cache before offering candidates." (funcall (citar--get-notes-config-property :action) key entry))) +;;;###autoload +(defun citar-open-links (keys) + "Open URL or DOI link associated with KEYS in a browser." + (interactive (list (citar-select-refs))) + ;; REVIEW this works, but should check for nil on select-resource + (if-let ((link (citar--select-resource keys :links t))) + (browse-url link) + (message "No link found for %s" keys))) + ;;;###autoload (defun citar-open-entry (key) "Open bibliographic entry associated with the KEY. @@ -1228,15 +1237,6 @@ directory as current buffer." (dolist (key keys) (citar--insert-bibtex key))))) -;;;###autoload -(defun citar-open-links (keys) - "Open URL or DOI link associated with KEYS in a browser." - (interactive (list (citar-select-refs))) - ;; REVIEW this works, but should check for nil on select-resource - (if-let ((link (citar--select-resource keys :links t))) - (browse-url link) - (message "No link found for %s" keys))) - ;;;###autoload (defun citar-insert-citation (keys &optional arg) "Insert citation for the KEYS. From a2679ba7c2f82a2c70ecf783b16fab70c992e19e Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 15:50:48 -0400 Subject: [PATCH 70/78] Use note plist for citar--open-notes --- citar.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/citar.el b/citar.el index 58c33844..13add6d3 100644 --- a/citar.el +++ b/citar.el @@ -1173,7 +1173,8 @@ With prefix, rebuild the cache before offering candidates." "Open notes associated with the KEYS." (interactive (list (citar-select-refs))) (dolist (key keys) - (funcall 'citar--open-notes key))) + (funcall + (citar--get-notes-config-property :action) key))) (defun citar--open-notes (key) "Open note(s) associated with KEY." From 27e74c8d73681a7b4f76bacc0cdbadc758b94d97 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 15:52:20 -0400 Subject: [PATCH 71/78] Remove citar--open-notes --- citar.el | 6 ------ 1 file changed, 6 deletions(-) diff --git a/citar.el b/citar.el index 13add6d3..7359e5dd 100644 --- a/citar.el +++ b/citar.el @@ -1176,12 +1176,6 @@ With prefix, rebuild the cache before offering candidates." (funcall (citar--get-notes-config-property :action) key))) -(defun citar--open-notes (key) - "Open note(s) associated with KEY." - (let ((entry (citar-get-entry key))) - (funcall - (citar--get-notes-config-property :action) key entry))) - ;;;###autoload (defun citar-open-links (keys) "Open URL or DOI link associated with KEYS in a browser." From aa050500d5e360263d895a4b4c612998b73ecd19 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 18:08:06 -0400 Subject: [PATCH 72/78] More note tweaks --- citar-file.el | 6 ++++-- citar.el | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/citar-file.el b/citar-file.el index 41b86b86..659992ed 100644 --- a/citar-file.el +++ b/citar-file.el @@ -41,6 +41,7 @@ (declare-function citar-get-value "citar") (declare-function citar--bibliography-files "citar") (declare-function citar--check-configuration "citar") +(declare-function citar--get-notes-config-property "citar") ;;;; File related variables @@ -342,10 +343,11 @@ need to scan the contents of DIRS in this case." (file-exists (file-exists-p file))) (find-file file) (if (and (null citar-notes-paths) - (equal citar-create-note-function + (equal (citar--get-notes-config-property :action) 'citar-org-format-note-default)) (error "You must set 'citar-notes-paths'") - (funcall citar-create-note-function key entry file)))) + (funcall + (citar--get-notes-config-property :create) key entry)))) (defun citar-file--get-note-files (keys) "Return list of notes associated with KEYS." diff --git a/citar.el b/citar.el index 7359e5dd..456dabae 100644 --- a/citar.el +++ b/citar.el @@ -73,13 +73,13 @@ (make-obsolete 'citar-field-with-value 'citar-get-field-with-value "1.0") ; now returns cons pair (make-obsolete 'citar--open-note 'citar-file--open-note "1.0") -(make-obsolete-variable - 'citar-format-note-function 'citar-create-note-function "1.0") +;(make-obsolete-variable +; 'citar-format-note-function "1.0") ;;; Declare variables and functions for byte compiler (defvar embark-default-action-overrides) -(declare-function marginalia-annotate-file "ext:marginalia") +(declare-function citar-org-format-note-default "citar-org") ;;; Variables @@ -281,8 +281,9 @@ If nil, single resources will open without prompting." `((citar-file . ,(list :name "Notes" :category 'file - :key-predicate #'citar-file-has-notes + :hasnote #'citar-file-has-notes :action #'citar-file--open-note + :create #'citar-org-format-note-default ; TODO remove? :items #'citar-file--get-note-files))) "The alist of notes backends available for configuration. @@ -293,7 +294,7 @@ plist has the following properties: :category the completion category - :key-predicate function to test for keys with notes + :hasnote function to test for keys with notes :action function to open a given note candidate @@ -916,8 +917,8 @@ value (the result of `citar-get-entries') rather than some smaller subset." (citar--has-resources-for-entries entries - (mapcar (lambda (fn) (funcall fn entries)) - citar-has-notes-functions))) + (funcall + (citar--get-notes-config-property :hasnote) entries))) (cl-defun citar-has-links (&key (entries (citar-get-entries))) @@ -1173,8 +1174,9 @@ With prefix, rebuild the cache before offering candidates." "Open notes associated with the KEYS." (interactive (list (citar-select-refs))) (dolist (key keys) - (funcall - (citar--get-notes-config-property :action) key))) + (let ((entry (citar-get-entry key))) + (funcall + (citar--get-notes-config-property :action) key entry)))) ;;;###autoload (defun citar-open-links (keys) @@ -1371,7 +1373,8 @@ VARIABLES should be the names of Citar customization variables." (seq-every-p #'stringp value)) (error "`%s' should be a list of strings: %S" variable `',value))) ((or 'citar-has-files-functions 'citar-get-files-functions - 'citar-has-notes-functions 'citar-open-note-functions + ; (citar--get-notes-config-property :hasnote) + ; (citar--get-notes-config-property :action) 'citar-file-parser-functions) (unless (and (listp value) (seq-every-p #'functionp value)) (error "`%s' should be a list of functions: %S" variable `',value))) From 27e7a081363624fb15fe7ed44a9f46a5947dc062 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 3 Jul 2022 21:55:27 -0400 Subject: [PATCH 73/78] README tweaks --- README.org | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/README.org b/README.org index b294f169..2d632965 100644 --- a/README.org +++ b/README.org @@ -73,7 +73,7 @@ This is the minimal configuration, and will work with any completing-read compli *** Embark -The =citar-embark= package adds contextual access actions in the minibuffer and at-point via the ~citar-embark-mode~ minor mode`. +The =citar-embark= package adds contextual access actions in the minibuffer and at-point via the ~citar-embark-mode~ minor mode. When using Embark, the Citar actions are generic, and work the same across org, markdown, and latex modes. #+BEGIN_SRC emacs-lisp @@ -223,30 +223,17 @@ You can save this history across sessions by adding =citar-history= to =savehist :CUSTOM_ID: refreshing-the-library-display :END: -=citar= uses a cache to speed up library display. -If a bib file changes, the cache should automatically update the next time you run a citar command. +Citar uses a cache to speed up library display. +If a bib file changes, the cache will automatically update the next time you run a Citar command. -The =citar-cache-refresh= command will reload the caches. +The =citar-cache-refresh= command will also reload the caches manually. ** Notes -Citar provides a ~citar-create-note-function~ variable, and a default function for org, which also works well with org-roam (v2 now supports org-cite). -You can configure the title display using the "note" template. - -You can also use the ~citar-open-note-functions~ variable to replace or augment the default with another; for example from org-roam-bibtex: - -#+BEGIN_SRC emacs-lisp -(setq citar-open-note-functions '(orb-citar-edit-note)) -#+END_SRC - -Since ~citar-open-note-functions~ is a list, you can also include multiple functions, to handle different note scenarios. - -Please note that if you choose to use org-roam-bibtex using the above configuration, you will need to set ~:immediate-finish t~ in the template that you use for bibliography notes in ~org-roam-capture-templates~. -Since ~citar-open-note-functions~ attempts multiple functions one after the other, this is needed to ensure the ~org-capture~ returns immediately without waiting for further user input. - -Citar also includes a ~citar-has-notes-functions~ variable, which specifies a list of functions, each of which returns a predicate function to test whether a reference has associated notes. -This function allows Citar to correctly format the completion UI candidates. -The default function only supports one-file-per-key notes. +Citar offers configurable note-taking and access integration. +The ~citar-notes-sources~ variable configures note backends, and ~citar-notes-source~ activates your chosen backend. +A backend primarily specifies functions to update the Citar display, to create the completion candidates, and to open existing and new notes. +See the docstrings for details. ** Files, file association and file-field parsing From ec6856d77fcdc95f2d159c485901e1623cf7c68b Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 26 Jun 2022 11:50:05 -0600 Subject: [PATCH 74/78] Improve bibliography cache invalidation logic. Bibliography files are now automatically re-parsed if their size or modification time have changed and their hash has changed. Also re-parse if the bibliography file has not changed but `citar--fields-to-parse` returns a different list. There should no longer be any need to manually refresh the cache, since calling any citar command will do that if needed. Therefore, remove the `citar-cache-refresh` command. Some other small changes: * Calculate the bibliography file hash only if its size and/or modification time have changed. * Update the preformatted strings if the format string has changed, even if the bibliography hasn't. --- citar-cache.el | 174 +++++++++++++++++++++++++++++++------------------ citar.el | 1 + 2 files changed, 110 insertions(+), 65 deletions(-) diff --git a/citar-cache.el b/citar-cache.el index a473479f..78dc4dab 100644 --- a/citar-cache.el +++ b/citar-cache.el @@ -32,7 +32,6 @@ (declare-function citar--get-template "citar") (declare-function citar--fields-to-parse "citar") -(declare-function citar--bibliography-files "citar") (defvar citar-ellipsis) @@ -57,28 +56,29 @@ being `citar-cache--bibliography' objects.") :read-only t :documentation "True filename of a bibliography, as returned by `file-truename'.") - (hash - nil - :documentation - "Hash of the file's contents, as returned by `buffer-hash'.") (buffers nil :documentation "List of buffers that require this bibliography.") + (props + nil + :documentation + "Plist with keys :size, :mtime, :hash, and :fields; attributes + of the cached file and the fields parsed from it.") (entries (make-hash-table :test 'equal) :documentation "Hash table mapping citation keys to bibliography entries, as returned by `parsebib-parse'.") + (format-string + nil + :documentation + "Format string used to generate pre-formatted strings.") (preformatted (make-hash-table :test 'equal) :documentation "Pre-formatted strings used to display bibliography entries; - see `citar--preformatter'.") - (format-string - nil - :documentation - "Format string used to generate pre-formatted strings.")) + see `citar--preformatter'.")) (defun citar-cache--get-bibliographies (filenames &optional buffer) @@ -95,9 +95,19 @@ longer needed by any other buffer. Return a list of `citar--bibliography' objects, one for each element of FILENAMES." (citar-cache--release-bibliographies filenames buffer) - (mapcar - (lambda (filename) (citar-cache--get-bibliography filename buffer)) - filenames)) + (let ((buffer (citar-cache--canonicalize-buffer buffer))) + (mapcar + (lambda (filename) + (let ((bib (citar-cache--get-bibliography filename))) + (prog1 bib + ;; Associate buffer with this bibliography: + (cl-pushnew buffer (citar-cache--bibliography-buffers bib)) + ;; Release bibliography when buffer is killed or changes major mode: + (when (bufferp buffer) + (with-current-buffer buffer + (dolist (hook '(change-major-mode-hook kill-buffer-hook)) + (add-hook hook #'citar-cache--release-bibliographies 0 'local))))))) + filenames))) (defun citar-cache--entry (key bibs) "Find the first entry for KEY in the bibliographies BIBS. @@ -126,33 +136,41 @@ all BIBS to their entries." ;;; Creating and deleting bibliography caches -(defun citar-cache--get-bibliography (filename &optional buffer) - "Return cached bibliography for FILENAME and associate it with BUFFER. -If FILENAME is not already cached, read and cache it. If BUFFER -is nil, use the current buffer. Otherwise, BUFFER should be a -buffer object or name that requires the bibliography FILENAME, or -a symbol like `global'." +(defun citar-cache--get-bibliography (filename &optional force-update) + "Return cached bibliography for FILENAME. + +If FILENAME is not already cached, read and cache it. If +FORCE-UPDATE is non-nil, re-read the bibliography even if it is +has not changed. + +Note: This function should not be called directly; use +`citar-get-bibliographies' instead. This function adds a +bibliography to the cache without associating it with any buffer, +so it will never be evicted from the cache. Use +`citar-cache--get-bibliographies' to ensure that the cached +bibliographies are removed when the associated buffers no longer +need them." (let* ((cached (gethash filename citar-cache--bibliographies)) - (bib (or cached (citar-cache--make-bibliography filename))) - (buffer (citar-cache--canonicalize-buffer buffer)) - (fmtstr (citar--get-template 'completion))) - (unless cached + (cachedprops (and cached (citar-cache--bibliography-props cached))) + (cachedfmtstr (and cached (citar-cache--bibliography-format-string cached))) + (props (citar-cache--get-bibliography-props filename cachedprops)) + (fmtstr (citar--get-template 'completion)) + (bib (or cached (citar-cache--make-bibliography filename)))) + (prog1 bib + ;; Set the format string so it's correct when updating bibliography (setf (citar-cache--bibliography-format-string bib) fmtstr) - (citar-cache--update-bibliography bib) - (puthash filename bib citar-cache--bibliographies)) - ;; Preformat strings if format has changed - (unless (equal-including-properties - fmtstr (citar-cache--bibliography-format-string bib)) - (setf (citar-cache--bibliography-format-string bib) fmtstr) - (citar-cache--preformat-bibliography bib)) - ;; Associate buffer with this bibliography: - (cl-pushnew buffer (citar-cache--bibliography-buffers bib)) - ;; Release bibliography when buffer is killed or changes major mode: - (unless (symbolp buffer) - (with-current-buffer buffer - (dolist (hook '(change-major-mode-hook kill-buffer-hook)) - (add-hook hook #'citar-cache--release-bibliographies 0 'local)))) - bib)) + ;; Update bibliography if needed or forced + (if (or force-update + (citar-cache--update-bibliography-p cachedprops props)) + (citar-cache--update-bibliography bib props) + ;; Otherwise, update props anyway in case mtime has changed: + (setf (citar-cache--bibliography-props bib) props) + ;; Pre-format if format string has changed even though bibliography hasn't + (unless (equal-including-properties fmtstr cachedfmtstr) + (citar-cache--preformat-bibliography bib))) + ;; Add bibliography to cache: + (unless cached + (puthash filename bib citar-cache--bibliographies))))) (defun citar-cache--release-bibliographies (&optional keep-filenames buffer) @@ -176,41 +194,67 @@ needed by any other buffer." (defun citar-cache--remove-bibliography (filename) "Remove bibliography cache entry for FILENAME." - ;; TODO Perform other needed actions, like removing filenotify watches + ;; TODO Perform other needed actions, like removing filenotify watches (remhash filename citar-cache--bibliographies)) ;;; Updating bibliographies -(defun citar-cache-refresh (&optional force) - "Refresh the bibliography cache. - -Unless FORCE is non-nil, the cached bib files are only reread if -modified since the last time they were updated." - (interactive) - (dolist (file (citar--bibliography-files)) - (let ((bib (citar-cache--get-bibliography file))) - (citar-cache--update-bibliography bib force)))) - -(defun citar-cache--update-bibliography (bib &optional force) +(defun citar-cache--get-bibliography-props (filename &optional oldprops) + "Return attributes to decide if bibliography FILENAME needs to be updated. +Return a plist with keys :size, :mtime, :hash, and :fields. +OLDPROPS, if given, should be a plist with the same keys. If +FILENAME has the same size and modification time as in OLDPROPS, +then assume that the hash value is also the same without +re-hashing the file contents." + (let* ((remote-file-name-inhibit-cache t) + (attr (file-attributes filename 'integer)) + (size (file-attribute-size attr)) + (mtime (file-attribute-modification-time attr)) + (fields (citar--fields-to-parse)) + (oldhash (plist-get oldprops :hash)) + (hash (if (and (stringp oldhash) + (equal size (plist-get oldprops :size)) + (equal mtime (plist-get oldprops :mtime))) + oldhash ; if size and mtime are unchanged, assume hash is the same + (with-temp-buffer + (insert-file-contents filename) + (buffer-hash))))) + `(:size ,size :mtime ,mtime :hash ,hash :fields ,fields))) + +(defun citar-cache--update-bibliography-p (oldprops newprops) + "Return whether bibliography needs to be updated. +Compare NEWPROPS with OLDPROPS and decide whether the file +contents have changed or the list of bibliography fields to be +parsed is different." + (not (and (equal (plist-get oldprops :size) (plist-get newprops :size)) + (equal (plist-get oldprops :hash) (plist-get newprops :hash)) + (equal (plist-get oldprops :fields) (plist-get newprops :fields))))) + +(defun citar-cache--update-bibliography (bib &optional props) "Update the bibliography BIB from the original file. -Unless FORCE is non-nil, the file is re-read only if it has been -modified since the last time BIB was updated." +PROPS should be a plist returned by +`citar-cache--get-bibliography-props'; if PROPS is unspecified; +use the value returned by that function. This argument is +provided in case that function has already been called so that +its return value can be reused. + +Only the bibliography fields listed in the :fields value of PROPS +are parsed. After updating, the `props' slot of BIB is set to +PROPS." (let* ((filename (citar-cache--bibliography-filename bib)) + (props (or props (citar-cache--get-bibliography-props filename))) (entries (citar-cache--bibliography-entries bib)) - (newhash (with-temp-buffer - (insert-file-contents filename) - (buffer-hash)))) - ;; TODO Also check file size and modification time before hashing? - ;; See `file-has-changed-p' in emacs 29, or `org-file-has-changed-p` - (when (or force (not (equal newhash (citar-cache--bibliography-hash bib)))) - ;; Update entries - (clrhash entries) - (parsebib-parse filename :entries entries :fields (citar--fields-to-parse)) - (setf (citar-cache--bibliography-hash bib) newhash) - ;; Update preformatted strings - (citar-cache--preformat-bibliography bib)))) + (messagestr (format "Updating bibliography %s" (abbreviate-file-name filename))) + (starttime (current-time))) + (message "%s..." messagestr) + (redisplay) ; Make sure message is displayed before Emacs gets busy parsing + (clrhash entries) + (parsebib-parse filename :entries entries :fields (plist-get props :fields)) + (setf (citar-cache--bibliography-props bib) props) + (citar-cache--preformat-bibliography bib) + (message "%s...done (%.3f seconds)" messagestr (float-time (time-since starttime))))) (defun citar-cache--preformat-bibliography (bib) @@ -224,7 +268,7 @@ modified since the last time BIB was updated." (lambda (citekey entry) (let* ((preformat (citar-format--preformat fieldspecs entry t citar-ellipsis)) - ;; CSL-JSOM lets citekey be an arbitrary string. Quote it if... + ;; CSL-JSON lets citekey be an arbitrary string. Quote it if... (keyquoted (if (or (string-empty-p citekey) ; ... it's empty, (= ?\" (aref citekey 0)) ; ... starts with ", (seq-contains-p citekey ?\s #'=)) ; ... or has a space diff --git a/citar.el b/citar.el index 456dabae..4324852a 100644 --- a/citar.el +++ b/citar.el @@ -232,6 +232,7 @@ the same width." ;;;; Citar actions and other miscellany +;; TODO this is no longer used. What to do with it? (defcustom citar-force-refresh-hook nil "Hook run when user forces a (re-) building of the candidates cache. This hook is only called when the user explicitly requests the From 73067692f0efc61c2cfe15add5e11426438c2892 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 3 Jul 2022 23:10:47 -0600 Subject: [PATCH 75/78] Fix indentation to make elisp-lint happy. --- citar-file.el | 4 ++-- citar.el | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/citar-file.el b/citar-file.el index 659992ed..f1f9b01c 100644 --- a/citar-file.el +++ b/citar-file.el @@ -326,8 +326,8 @@ need to scan the contents of DIRS in this case." (defun citar-file--get-notes-hash (&optional keys) "Return hash-table with KEYS with file notes." (citar-file--directory-files - citar-notes-paths keys citar-file-note-extensions - citar-file-additional-files-separator)) + citar-notes-paths keys citar-file-note-extensions + citar-file-additional-files-separator)) (defun citar-file-has-notes (&optional _entries) "Return predicate testing whether cite key has associated notes." diff --git a/citar.el b/citar.el index 4324852a..cb713506 100644 --- a/citar.el +++ b/citar.el @@ -73,8 +73,7 @@ (make-obsolete 'citar-field-with-value 'citar-get-field-with-value "1.0") ; now returns cons pair (make-obsolete 'citar--open-note 'citar-file--open-note "1.0") -;(make-obsolete-variable -; 'citar-format-note-function "1.0") +;;(make-obsolete-variable 'citar-format-note-function "1.0") ;;; Declare variables and functions for byte compiler @@ -642,16 +641,16 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (if-let ((resources (citar--get-resource-candidates keys :files files :notes notes :links links))) - (completing-read - "Select resource: " - (lambda (string predicate action) - ;; REVIEW how to hook in annotation functions here by category? - (if (eq action 'metadata) - `(metadata - (group-function . citar--select-group-related-resources) - (annotation-function . citar--multi-annotate) - (category . multi-category)) - (complete-with-action action resources string predicate)))))) + (completing-read + "Select resource: " + (lambda (string predicate action) + ;; REVIEW how to hook in annotation functions here by category? + (if (eq action 'metadata) + `(metadata + (group-function . citar--select-group-related-resources) + (annotation-function . citar--multi-annotate) + (category . multi-category)) + (complete-with-action action resources string predicate)))))) (defun citar--select-group-related-resources (resource transform) "Group RESOURCE by type or TRANSFORM." @@ -1374,8 +1373,8 @@ VARIABLES should be the names of Citar customization variables." (seq-every-p #'stringp value)) (error "`%s' should be a list of strings: %S" variable `',value))) ((or 'citar-has-files-functions 'citar-get-files-functions - ; (citar--get-notes-config-property :hasnote) - ; (citar--get-notes-config-property :action) + ; (citar--get-notes-config-property :hasnote) + ; (citar--get-notes-config-property :action) 'citar-file-parser-functions) (unless (and (listp value) (seq-every-p #'functionp value)) (error "`%s' should be a list of functions: %S" variable `',value))) From 57f2dbe107f77321f1421c0db8f7d92ce8ea67b1 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Mon, 4 Jul 2022 03:56:38 -0400 Subject: [PATCH 76/78] Remove rebuild hook; no longer needed --- citar.el | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/citar.el b/citar.el index cb713506..fb6c4075 100644 --- a/citar.el +++ b/citar.el @@ -231,16 +231,6 @@ the same width." ;;;; Citar actions and other miscellany -;; TODO this is no longer used. What to do with it? -(defcustom citar-force-refresh-hook nil - "Hook run when user forces a (re-) building of the candidates cache. -This hook is only called when the user explicitly requests the -cache to be rebuilt. It is intended for \"heavy\" operations -which recreate entire bibliography files using an external -reference manager like Zotero or JabRef." - :group 'citar - :type 'hook) - (defcustom citar-default-action #'citar-open "The default action for the `citar-at-point' command. Should be a function that takes one argument, a list with each @@ -524,8 +514,7 @@ FILTER: if non-nil, should be a predicate function taking "Select bibliographic references. Call `citar-select-ref' with argument `:multiple'; see its -documentation for the return value and the meaning of -REBUILD-CACHE and FILTER." +documentation for the return value." (car (citar-select-refs :multiple nil :filter filter))) (defun citar--multiple-completion-table (selected-hash candidates filter) @@ -1143,9 +1132,7 @@ For use with `embark-act-all'." ;;;###autoload (defun citar-attach-files (key-or-keys) - "Attach library file associated with KEY-OR-KEYS to outgoing MIME message. - -With prefix, rebuild the cache before offering candidates." + "Attach library file associated with KEY-OR-KEYS to outgoing MIME message." (interactive (list (citar-select-ref))) (let ((embark-default-action-overrides '((file . mml-attach-file)))) (citar--library-file-action key-or-keys #'mml-attach-file))) @@ -1189,16 +1176,14 @@ With prefix, rebuild the cache before offering candidates." ;;;###autoload (defun citar-open-entry (key) - "Open bibliographic entry associated with the KEY. -With prefix, rebuild the cache before offering candidates." + "Open bibliographic entry associated with the KEY." (interactive (list (citar-select-ref))) (when-let ((bibtex-files (citar--bibliography-files))) (bibtex-search-entry key t nil t))) ;;;###autoload (defun citar-insert-bibtex (keys) - "Insert bibliographic entry associated with the KEYS. -With prefix, rebuild the cache before offering candidates." + "Insert bibliographic entry associated with the KEYS." (interactive (list (citar-select-refs))) (dolist (key keys) (citar--insert-bibtex key))) @@ -1290,8 +1275,7 @@ ARG is forwarded to the mode-specific insertion function given in ;;;###autoload (defun citar-insert-keys (keys) - "Insert KEYS citekeys. -With prefix, rebuild the cache before offering candidates." + "Insert KEYS citekeys." (interactive (list (citar-select-refs))) (citar--major-mode-function 'insert-keys From 521e7dc742933e251c2ea3339d9420c78e3a095b Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Mon, 4 Jul 2022 04:28:51 -0400 Subject: [PATCH 77/78] Rename get-note-config Also add convenience functions for adding and removing notes backends. --- citar-file.el | 6 +++--- citar.el | 33 +++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/citar-file.el b/citar-file.el index f1f9b01c..2975e914 100644 --- a/citar-file.el +++ b/citar-file.el @@ -41,7 +41,7 @@ (declare-function citar-get-value "citar") (declare-function citar--bibliography-files "citar") (declare-function citar--check-configuration "citar") -(declare-function citar--get-notes-config-property "citar") +(declare-function citar--get-notes-config "citar") ;;;; File related variables @@ -343,11 +343,11 @@ need to scan the contents of DIRS in this case." (file-exists (file-exists-p file))) (find-file file) (if (and (null citar-notes-paths) - (equal (citar--get-notes-config-property :action) + (equal (citar--get-notes-config :action) 'citar-org-format-note-default)) (error "You must set 'citar-notes-paths'") (funcall - (citar--get-notes-config-property :create) key entry)))) + (citar--get-notes-config :create) key entry)))) (defun citar-file--get-note-files (keys) "Return list of notes associated with KEYS." diff --git a/citar.el b/citar.el index fb6c4075..09f02989 100644 --- a/citar.el +++ b/citar.el @@ -576,7 +576,6 @@ HISTORY is the `completing-read' history argument." (equal item ""))))) (hash-table-keys selected-hash))) - (defun citar--add-notep-prop (candidate) "Add a note resource CANDIDATE with 'notep t'." (propertize candidate 'notep t)) @@ -595,8 +594,8 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (cons 'url (citar-get-links keys)))) (notesource (when notes - (let* ((cat (citar--get-notes-config-property :category)) - (items (citar--get-notes-config-property :items)) + (let* ((cat (citar--get-notes-config :category)) + (items (citar--get-notes-config :items)) (items (if (functionp items) (funcall items keys) items)) (items (mapcar #'citar--add-notep-prop items))) (cons cat items)))) @@ -618,8 +617,8 @@ Optionally constrain to FILES, NOTES, and/or LINKS." "Annotate candidate CAND with `consult--multi' type." ;; Adapted from 'consult' (let* ((nodecat (car (get-text-property 0 'multi-category cand))) - (notecat (citar--get-notes-config-property :category)) - (annotate (citar--get-notes-config-property :annotate)) + (notecat (citar--get-notes-config :category)) + (annotate (citar--get-notes-config :annotate)) (ann (when (and annotate (string= nodecat notecat)) (funcall annotate (cdr (get-text-property 0 'multi-category cand)))))) ann)) @@ -650,7 +649,7 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (let ((cat (car (get-text-property 0 'multi-category resource))) (notep (get-text-property 0 'notep resource))) ;; If note, assign to note group; otherwise use completion category. - (if notep (citar--get-notes-config-property :name) + (if notep (citar--get-notes-config :name) (pcase cat ('file "Library Files") ('url "Links")))))) @@ -806,14 +805,24 @@ The value is transformed using `citar-display-transform-functions'" ;;;; File, notes, and links -(defun citar--get-notes-config-property (property) +(defun citar--get-notes-config (property) "Return PROPERTY value for configured notes backend." (plist-get (alist-get citar-notes-source citar-notes-sources) property)) +(defun citar-register-notes-source (name config) + "Register note backend. + +NAME is a symbol, and CONFIG is a plist." + (add-to-list 'citar-notes-sources (cons name config))) + +(defun citar-remove-notes-source (name) + "Remove note backend NAME." + (assoc-delete-all name citar-notes-sources)) + (defun citar-get-notes (keys) "Return list of notes associated with KEYS." - (funcall (citar--get-notes-config-property :items) keys)) + (funcall (citar--get-notes-config :items) keys)) (cl-defun citar-get-files (key-or-keys &key (entries (citar-get-entries))) "Return list of files associated with KEY-OR-KEYS in ENTRIES. @@ -907,7 +916,7 @@ smaller subset." (citar--has-resources-for-entries entries (funcall - (citar--get-notes-config-property :hasnote) entries))) + (citar--get-notes-config :hasnote) entries))) (cl-defun citar-has-links (&key (entries (citar-get-entries))) @@ -1163,7 +1172,7 @@ For use with `embark-act-all'." (dolist (key keys) (let ((entry (citar-get-entry key))) (funcall - (citar--get-notes-config-property :action) key entry)))) + (citar--get-notes-config :action) key entry)))) ;;;###autoload (defun citar-open-links (keys) @@ -1357,8 +1366,8 @@ VARIABLES should be the names of Citar customization variables." (seq-every-p #'stringp value)) (error "`%s' should be a list of strings: %S" variable `',value))) ((or 'citar-has-files-functions 'citar-get-files-functions - ; (citar--get-notes-config-property :hasnote) - ; (citar--get-notes-config-property :action) + ; (citar--get-notes-config :hasnote) + ; (citar--get-notes-config :action) 'citar-file-parser-functions) (unless (and (listp value) (seq-every-p #'functionp value)) (error "`%s' should be a list of functions: %S" variable `',value))) From a3bee3ce1c3820422173313cb591b25265b101c3 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Mon, 4 Jul 2022 05:00:23 -0400 Subject: [PATCH 78/78] README tweaks --- README.org | 6 +++--- citar-file.el | 1 + citar.el | 5 ++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.org b/README.org index 2d632965..98693dd2 100644 --- a/README.org +++ b/README.org @@ -225,15 +225,15 @@ You can save this history across sessions by adding =citar-history= to =savehist Citar uses a cache to speed up library display. If a bib file changes, the cache will automatically update the next time you run a Citar command. - -The =citar-cache-refresh= command will also reload the caches manually. +Note that cached data preformatted completion candidates are independently tracked by file. +So, for example, if you have one very large bibliography file that changes a lot, you might consider splitting into one large file that is more stable, and one-or-more smaller ones that change more frequently. ** Notes Citar offers configurable note-taking and access integration. The ~citar-notes-sources~ variable configures note backends, and ~citar-notes-source~ activates your chosen backend. A backend primarily specifies functions to update the Citar display, to create the completion candidates, and to open existing and new notes. -See the docstrings for details. +See the ~citar-notes-sources~ docstring for details, and the =citar-register-note-source= and =citar-remove-note-source= convenience functions. ** Files, file association and file-field parsing diff --git a/citar-file.el b/citar-file.el index 2975e914..7e68759b 100644 --- a/citar-file.el +++ b/citar-file.el @@ -331,6 +331,7 @@ need to scan the contents of DIRS in this case." (defun citar-file-has-notes (&optional _entries) "Return predicate testing whether cite key has associated notes." + ;; REVIEW why this optional arg when not needed? (let ((files (citar-file--get-notes-hash))) (lambda (key) (gethash key files)))) diff --git a/citar.el b/citar.el index 09f02989..85dd8b3a 100644 --- a/citar.el +++ b/citar.el @@ -513,8 +513,8 @@ FILTER: if non-nil, should be a predicate function taking (cl-defun citar-select-ref (&key filter) "Select bibliographic references. -Call `citar-select-ref' with argument `:multiple'; see its -documentation for the return value." +Call 'citar-select-ref' with argument ':multiple, and optional +FILTER; see its documentation for the return value." (car (citar-select-refs :multiple nil :filter filter))) (defun citar--multiple-completion-table (selected-hash candidates filter) @@ -632,7 +632,6 @@ Optionally constrain to FILES, NOTES, and/or LINKS." (completing-read "Select resource: " (lambda (string predicate action) - ;; REVIEW how to hook in annotation functions here by category? (if (eq action 'metadata) `(metadata (group-function . citar--select-group-related-resources)