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