dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

denote-sort.el (8563B)


      1 ;;; denote-sort.el ---  Sort Denote files based on a file name component -*- lexical-binding: t -*-
      2 
      3 ;; Copyright (C) 2023-2024  Free Software Foundation, Inc.
      4 
      5 ;; Author: Protesilaos Stavrou <info@protesilaos.com>
      6 ;; Maintainer: Protesilaos Stavrou <info@protesilaos.com>
      7 ;; URL: https://github.com/protesilaos/denote
      8 
      9 ;; This file is NOT part of GNU Emacs.
     10 
     11 ;; This program is free software; you can redistribute it and/or modify
     12 ;; it under the terms of the GNU General Public License as published by
     13 ;; the Free Software Foundation, either version 3 of the License, or
     14 ;; (at your option) any later version.
     15 ;;
     16 ;; This program is distributed in the hope that it will be useful,
     17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     19 ;; GNU General Public License for more details.
     20 ;;
     21 ;; You should have received a copy of the GNU General Public License
     22 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     23 
     24 ;;; Commentary:
     25 ;;
     26 ;; Sort Denote files based on their file name components, namely, the
     27 ;; signature, title, or keywords.
     28 
     29 ;;; Code:
     30 
     31 (require 'denote)
     32 
     33 (defgroup denote-sort nil
     34   "Sort Denote files based on a file name component."
     35   :group 'denote
     36   :link '(info-link "(denote) Top")
     37   :link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
     38 
     39 (defvar denote-sort-comparison-function #'string-collate-lessp
     40   "String comparison function used by `denote-sort-files' subroutines.")
     41 
     42 (defvar denote-sort-components '(title keywords signature identifier)
     43   "List of sorting keys applicable for `denote-sort-files' and related.")
     44 
     45 ;; NOTE 2023-12-04: We can have compound sorting algorithms such as
     46 ;; title+signature, but I want to keep this simple for the time being.
     47 ;; Let us first hear from users to understand if there is a real need
     48 ;; for such a feature.
     49 (defmacro denote-sort--define-lessp (component)
     50   "Define function to sort by COMPONENT."
     51   (let ((retrieve-fn (intern (format "denote-retrieve-filename-%s" component))))
     52     `(defun ,(intern (format "denote-sort-%s-lessp" component)) (file1 file2)
     53        ,(format
     54          "Return smallest between FILE1 and FILE2 based on their %s.
     55 The comparison is done with `denote-sort-comparison-function' between the
     56 two title values."
     57          component)
     58        (let* ((one (,retrieve-fn file1))
     59               (two (,retrieve-fn file2))
     60               (one-empty-p (or (null one) (string-empty-p one)))
     61               (two-empty-p (or (null two) (string-empty-p two))))
     62          (cond
     63           (one-empty-p nil)
     64           ((and (not one-empty-p) two-empty-p) one)
     65           (t (funcall denote-sort-comparison-function one two)))))))
     66 
     67 ;; TODO 2023-12-04: Subject to the above NOTE, we can also sort by
     68 ;; directory and by file length.
     69 (denote-sort--define-lessp title)
     70 (denote-sort--define-lessp keywords)
     71 (denote-sort--define-lessp signature)
     72 
     73 ;;;###autoload
     74 (defun denote-sort-files (files component &optional reverse)
     75   "Returned sorted list of Denote FILES.
     76 
     77 With COMPONENT as a symbol among `denote-sort-components',
     78 sort files based on the corresponding file name component.
     79 
     80 With COMPONENT as a nil value keep the original date-based
     81 sorting which relies on the identifier of each file name.
     82 
     83 With optional REVERSE as a non-nil value, reverse the sort order."
     84   (let* ((files-to-sort (copy-sequence files))
     85          (sort-fn (when component
     86                     (pcase component
     87                       ('title #'denote-sort-title-lessp)
     88                       ('keywords #'denote-sort-keywords-lessp)
     89                       ('signature #'denote-sort-signature-lessp))))
     90          (sorted-files (if sort-fn (sort files sort-fn) files-to-sort)))
     91     (if reverse
     92         (reverse sorted-files)
     93       sorted-files)))
     94 
     95 (defun denote-sort-get-directory-files (files-matching-regexp sort-by-component &optional reverse omit-current)
     96   "Return sorted list of files in variable `denote-directory'.
     97 
     98 With FILES-MATCHING-REGEXP as a string limit files to those
     99 matching the given regular expression.
    100 
    101 With SORT-BY-COMPONENT as a symbol among `denote-sort-components',
    102 pass it to `denote-sort-files' to sort by the corresponding file
    103 name component.
    104 
    105 With optional REVERSE as a non-nil value, reverse the sort order.
    106 
    107 With optional OMIT-CURRENT, do not include the current file in
    108 the list."
    109   (denote-sort-files
    110    (denote-directory-files files-matching-regexp omit-current)
    111    sort-by-component
    112    reverse))
    113 
    114 (defun denote-sort-get-links (files-matching-regexp sort-by-component current-file-type id-only &optional reverse)
    115   "Return sorted typographic list of links for FILES-MATCHING-REGEXP.
    116 
    117 With FILES-MATCHING-REGEXP as a string, match files stored in the
    118 variable `denote-directory'.
    119 
    120 With SORT-BY-COMPONENT as a symbol among `denote-sort-components',
    121 sort FILES-MATCHING-REGEXP by the given Denote file name
    122 component.  If SORT-BY-COMPONENT is nil or an unknown non-nil
    123 value, default to the identifier-based sorting.
    124 
    125 With CURRENT-FILE-TYPE as a symbol among those specified in
    126 `denote-file-type' (or the `car' of each element in
    127 `denote-file-types'), format the link accordingly.  With a nil or
    128 unknown non-nil value, default to the Org notation.
    129 
    130 With ID-ONLY as a non-nil value, produce links that consist only
    131 of the identifier, thus deviating from CURRENT-FILE-TYPE.
    132 
    133 With optional REVERSE as a non-nil value, reverse the sort order."
    134   (denote-link--prepare-links
    135    (denote-sort-get-directory-files files-matching-regexp sort-by-component reverse)
    136    current-file-type
    137    id-only))
    138 
    139 (defvar denote-sort-component-history nil
    140   "Minibuffer history of `denote-sort-component-prompt'.")
    141 
    142 (defalias 'denote-sort--component-hist 'denote-sort-component-history
    143   "Compatibility alias for `denote-sort-component-history'.")
    144 
    145 (defun denote-sort-component-prompt ()
    146   "Prompt `denote-sort-files' for sorting key among `denote-sort-components'."
    147   (let ((default (car denote-sort-component-history)))
    148     (intern
    149      (completing-read
    150       (format-prompt "Sort by file name component" default)
    151       denote-sort-components nil :require-match
    152       nil 'denote-sort-component-history default))))
    153 
    154 (defvar-local denote-sort--dired-buffer nil
    155   "Buffer object of current `denote-sort-dired'.")
    156 
    157 ;;;###autoload
    158 (defun denote-sort-dired (files-matching-regexp sort-by-component reverse)
    159   "Produce Dired dired-buffer with sorted files from variable `denote-directory'.
    160 When called interactively, prompt for FILES-MATCHING-REGEXP,
    161 SORT-BY-COMPONENT, and REVERSE.
    162 
    163 1. FILES-MATCHING-REGEXP limits the list of Denote files to
    164    those matching the provided regular expression.
    165 
    166 2. SORT-BY-COMPONENT sorts the files by their file name
    167    component (one among `denote-sort-components').
    168 
    169 3. REVERSE is a boolean to reverse the order when it is a non-nil value.
    170 
    171 When called from Lisp, the arguments are a string, a keyword, and
    172 a non-nil value, respectively."
    173   (interactive
    174    (list
    175     (denote-files-matching-regexp-prompt)
    176     (denote-sort-component-prompt)
    177     (y-or-n-p "Reverse sort? ")))
    178   (if-let ((default-directory (denote-directory))
    179            (files (denote-sort-get-directory-files files-matching-regexp sort-by-component reverse))
    180            ;; NOTE 2023-12-04: Passing the FILES-MATCHING-REGEXP as
    181            ;; buffer-name produces an error if the regexp contains a
    182            ;; wildcard for a directory. I can reproduce this in emacs
    183            ;; -Q and am not sure if it is a bug. Anyway, I will report
    184            ;; it upstream, but even if it is fixed we cannot use it
    185            ;; for now (whatever fix will be available for Emacs 30+).
    186            (denote-sort-dired-buffer-name (format "Denote sort `%s' by `%s'" files-matching-regexp sort-by-component))
    187            (buffer-name (format "Denote sort by `%s' at %s" sort-by-component (format-time-string "%T"))))
    188       (let ((dired-buffer (dired (cons buffer-name (mapcar #'file-relative-name files)))))
    189         (setq denote-sort--dired-buffer dired-buffer)
    190         (with-current-buffer dired-buffer
    191           (setq-local revert-buffer-function
    192                       (lambda (&rest _)
    193                         (kill-buffer dired-buffer)
    194                         (denote-sort-dired files-matching-regexp sort-by-component reverse))))
    195         ;; Because of the above NOTE, I am printing a message.  Not
    196         ;; what I want, but it is better than nothing...
    197         (message denote-sort-dired-buffer-name))
    198     (message "No matching files for: %s" files-matching-regexp)))
    199 
    200 (provide 'denote-sort)
    201 ;;; denote-sort.el ends here