diff --git a/citar.el b/citar.el index 38606289..350e0624 100644 --- a/citar.el +++ b/citar.el @@ -349,6 +349,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,13 +386,24 @@ 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. @@ -419,7 +436,15 @@ and other completion functions." (or (null predicate) (funcall predicate cand)))))))) (complete-with-action action candidates string predicate)))))) -(cl-defun citar-select-ref (&optional &key rebuild-cache multiple filter) +(defun citar-select-ref (&optional _multiple _filter) + "Select reference, return citekey." + (let* ((table + (or (citar--ref-completion-table) + (user-error "No bibliography set"))) + (choice (completing-read "Ref: " table))) + (gethash choice table))) + +(cl-defun citaro-select-ref (&optional &key _rebuild-cache multiple filter) "Select bibliographic references. A wrapper around 'completing-read' that returns (KEY . ENTRY), @@ -448,14 +473,14 @@ FILTER: if non-nil, should be a predicate function taking :filter (lambda (_key entry) (when-let ((keywords (assoc-default \"keywords\" entry))) (string-match-p \"foo\" keywords))))" - (let* ((candidates (citar--get-candidates rebuild-cache)) + (let* ((candidates (citar--ref-completion-table)) (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 + (keys (seq-mapcat ;; Find citation key-entry of selected candidate. ;; CHOICE is either the formatted candidate string, or the citation @@ -467,14 +492,14 @@ FILTER: if non-nil, should be a predicate function taking (if-let ((cand (seq-find (lambda (cand) (member choice (seq-take cand 2))) candidates))) - (list (cdr cand)) + (list 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)))) + (when multiple keys keys))) (cl-defun citar-select-refs (&optional &key rebuild-cache filter) "Select bibliographic references. @@ -572,6 +597,59 @@ 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))) + (cond + ((null entries) nil) ; no bibliography files + ((gethash entries citar--completion-cache) + citar--completion-cache) ; REVIEW ? + (t + (clrhash citar--completion-cache) + (dolist (key (citar--all-keys)) + (let ((completion (citar--get-value key "title"))) ; TODO hook up string formatting + (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, @@ -599,9 +677,21 @@ 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))) +(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 (key-or-entry field) + "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 ." @@ -778,38 +868,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 @@ -818,40 +876,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." diff --git a/citarn.el b/citarn.el new file mode 100644 index 00000000..2e15521b --- /dev/null +++ b/citarn.el @@ -0,0 +1,140 @@ +;;; citarn.el --- Stripped down test of alt citar -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 Bruce D'Arcus +;; +;; Author: Bruce D'Arcus +;; Maintainer: Bruce D'Arcus +;; Created: June 06, 2022 +;; Modified: June 06, 2022 +;; Version: 0.0.1 +;; Package-Requires: ((emacs "28.1")) +;; +;; This file is not part of GNU Emacs. +;; +;;; Commentary: +;; +;; A minimal experiment in an alternative structure for citar caching and the +;; API. +;; +;; Code is commented. +;; +;;; Code: + +(require 'parsebib) +(require 'citar) + +;; Internal variables + +;; Most of this design is adapted from org-mode 'oc-basic', +;; written by Nicolas Goaziou. + +(defvar citarn--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 citarn--completion-cache (make-hash-table :test #'equal) + "Hash with key as completion string, value as citekey.") + +;; Internal functions + +(defun citarn--all-keys () + "List all keys available in current bibliography." + (seq-mapcat (pcase-lambda (`(,_ . ,entries)) + (map-keys entries)) + (citarn--parse-bibliography))) + +(defun citarn--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 (citarn--parse-bibliography))) + (cond + ((null entries) nil) ; no bibliography files + ((gethash entries citarn--completion-cache) + citarn--completion-cache) ; REVIEW ? + (t + (clrhash citarn--completion-cache) + (dolist (key (citarn--all-keys)) + (let ((completion (citarn--get-value key "title"))) ; TODO hook up string formatting + (puthash completion key citarn--completion-cache))) + (unless (map-empty-p citarn--completion-cache) ; no key + (puthash entries t citarn--completion-cache) ; REVIEW ? + citarn--completion-cache))))) + +;; adapted from 'org-cite-basic--parse-bibliography' +(defvar citarn--file-id-cache nil + "Hash table linking files to their hash.") + +(defun citarn--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 citarn--file-id-cache) + (setq citarn--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 citarn--file-id-cache))) + (insert-file-contents file) + (puthash file (md5 (current-buffer)) citarn--file-id-cache)) + (let* ((file-id (cons file (gethash file citarn--file-id-cache))) + (entries + (or (cdr (assoc file-id citarn--bibliography-cache)) + (let ((table (parsebib-parse file))) + (push (cons file-id table) citarn--bibliography-cache) + table)))) + (push (cons file entries) results))))) + results)) + +(defun citarn--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) (citarn--parse-bibliography)) + (let ((entry (gethash key entries))) + (when entry (throw :found entry)))) + nil)) + +(defun citarn--get-value (key-or-entry field) + "Return FIELD value for KEY-OR-ENTRY." + (let ((entry (if (stringp key-or-entry) + (citarn--get-entry key-or-entry) + key-or-entry))) + (cdr (assoc-string field entry)))) + +(defun citarn-select-ref () + "Select reference, return citekey." + (let* ((table + (or (citarn--ref-completion-table) + (user-error "No bibliography set"))) + (choice (completing-read "Ref: " table))) + (gethash choice table))) + +;; Interactive commands + +(defun citarn-example () + "Return title as message." + (interactive) + (let* ((choice (citarn-select-ref)) + (title (citarn--get-value choice "title"))) + (message title))) + +(citarn-example) + +(provide 'citarn) +;;; citarn.el ends here