branch: externals/denote commit 035e1452febdc5b5add7d0645f90b265284298cc Author: Protesilaos Stavrou <i...@protesilaos.com> Commit: Protesilaos Stavrou <i...@protesilaos.com>
Merge denote-sort.el into denote.el This is part of the wider reorganisation of the project, as discussed in issue 543: <https://github.com/protesilaos/denote/issues/543>. --- README.org | 24 +++-- denote-sort.el | 335 --------------------------------------------------------- denote.el | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 321 insertions(+), 344 deletions(-) diff --git a/README.org b/README.org index 88d7e9f4ee..e8b07ef16c 100644 --- a/README.org +++ b/README.org @@ -4262,16 +4262,20 @@ is also available ([[#h:8ec774f4-4154-4599-b8cf-fa767e18d22d][Find a relative of :CUSTOM_ID: h:9fe01e63-f34f-4479-8713-f162a5ca865e :END: -The =denote-sort.el= file is an optional extension to the core -functionality of Denote, which empowers users to sort files by the -given file name component ([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file-naming scheme]]). +[ As part of {{{development-version}}}, the code of =denote-sort.el= + is merged into =denote.el= as part of a wider reorganisation of the + project. ] + +The =denote.el= file contains functions which empowers user or +developers to sort files by the given file name component ([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file-naming scheme]]). #+findex: denote-sort-dired -The command ~denote-sort-dired~ produces a Dired file listing with a -flat, filtered, and sorted set of files from the ~denote-directory~ -([[#h:c958e087-1d23-4a25-afdd-db7bf5606b4c][Define a sorting function per component]]). It does so by a series of -prompts, which can be configured by the user option -~denote-sort-dired-extra-prompts~ ([[#h:a34228cb-484f-48fe-9cbc-8e41f313127b][Configure what extra prompts ~denote-sort-dired~ issues]]). +#+findex: denote-dired +The command ~denote-sort-dired~ (alias ~denote-dired~) produces a +Dired file listing with a flat, filtered, and sorted set of files from +the ~denote-directory~ ([[#h:c958e087-1d23-4a25-afdd-db7bf5606b4c][Define a sorting function per component]]). It +does so by a series of prompts, which can be configured by the user +option ~denote-sort-dired-extra-prompts~ ([[#h:a34228cb-484f-48fe-9cbc-8e41f313127b][Configure what extra prompts ~denote-sort-dired~ issues]]). The out-of-the-box behaviour of ~denote-sort-dired~ is as follows: @@ -4289,8 +4293,10 @@ The out-of-the-box behaviour of ~denote-sort-dired~ is as follows: The resulting Dired listing is a regular Dired buffer, unlike that of ~dired-virtual-mode~ ([[#h:d35d8d41-f51b-4139-af8f-9c8cc508e35b][Use ~dired-virtual-mode~ for arbitrary file listings]]). +#+findex: denote-sort-files The dynamic Org blocks that Denote defines to insert file contents -also use this feature ([[#h:f15fa143-5036-416f-9bff-1bcabbb03456][Org dynamic block to insert file contents]]). +also use this feature internally by means of the non-interactive +function ~denote-sort-files~ ([[#h:f15fa143-5036-416f-9bff-1bcabbb03456][Org dynamic block to insert file contents]]). ** Configure what extra prompts ~denote-sort-dired~ issues :PROPERTIES: diff --git a/denote-sort.el b/denote-sort.el deleted file mode 100644 index 0ee5be5bd8..0000000000 --- a/denote-sort.el +++ /dev/null @@ -1,335 +0,0 @@ -;;; denote-sort.el --- Sort Denote files based on a file name component -*- lexical-binding: t -*- - -;; Copyright (C) 2023-2025 Free Software Foundation, Inc. - -;; Author: Protesilaos Stavrou <i...@protesilaos.com> -;; Maintainer: Protesilaos Stavrou <i...@protesilaos.com> -;; URL: https://github.com/protesilaos/denote - -;; 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 <https://www.gnu.org/licenses/>. - -;;; Commentary: -;; -;; Sort Denote files based on their file name components, namely, the -;; signature, title, or keywords. - -;;; Code: - -(require 'denote) - -(defgroup denote-sort nil - "Sort Denote files based on a file name component." - :group 'denote - :link '(info-link "(denote) Top") - :link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote")) - -(defconst denote-sort-comparison-fallback-function #'string-collate-lessp - "String comparison function used by `denote-sort-files' subroutines.") - -(defconst denote-sort-components '(title keywords signature identifier) - "List of sorting keys applicable for `denote-sort-files' and related.") - -(defcustom denote-sort-identifier-comparison-function denote-sort-comparison-fallback-function - "Function to sort the DATE/IDENTIFIER component in file names. -The function accepts two arguments and must return a non-nil value if -the first argument is smaller than the second one." - :type 'function - :package-version '(denote . "3.2.0") - :group 'denote-sort) - -(defcustom denote-sort-title-comparison-function denote-sort-comparison-fallback-function - "Function to sort the TITLE component in file names. -The function accepts two arguments and must return a non-nil value if -the first argument is smaller than the second one." - :type 'function - :package-version '(denote . "3.1.0") - :group 'denote-sort) - -(defcustom denote-sort-keywords-comparison-function denote-sort-comparison-fallback-function - "Function to sort the KEYWORDS component in file names. -The function accepts two arguments and must return a non-nil value if -the first argument is smaller than the second one." - :type 'function - :package-version '(denote . "3.1.0") - :group 'denote-sort) - -(defcustom denote-sort-signature-comparison-function denote-sort-comparison-fallback-function - "Function to sort the SIGNATURE component in file names. -The function accepts two arguments and must return a non-nil value if -the first argument is smaller than the second one." - :type 'function - :package-version '(denote . "3.1.0") - :group 'denote-sort) - -(defcustom denote-sort-dired-extra-prompts '(sort-by-component reverse-sort) - "Determine what `denote-sort-dired' prompts for beside a search query. -This concerns the additional prompts issued by `denote-sort-dired' about -whether to sort by a given file name component and to then reverse the -sort. - -The value is a list of symbols, which can include the symbols -`sort-by-component', `reverse-sort', and `exclude-regexp'. The order is -significant, with the leftmost symbol coming first. - -These symbols correspond to the following: - -- A choice to select the file name component to sort by. -- A yes or no prompt on whether to reverse the sorting. -- A string (or regular expression) of files to be excluded from the results. - -If the value is nil, skip all prompts. In this scenario, the sorting is -done according to `denote-sort-dired-default-sort-component' and -`denote-sort-dired-default-reverse-sort'." - :type '(radio (const :tag "Do not prompt for anything" nil) - (set :tag "Available prompts" :greedy t - (const :tag "Sort by file name component" sort-by-component) - (const :tag "Reverse the sort" reverse-sort) - (const :tag "Exclude files matching regexp" exclude-regexp))) - :package-version '(denote . "3.2.0") - :group 'denote-sort) - -(defcustom denote-sort-dired-default-sort-component 'identifier - "Set the default file name component to sort by. -This is used only if `denote-sort-dired-extra-prompts' omits the -minibuffer prompt for which file name component to sort by." - :type '(radio - (const :tag "Sort by identifier (default)" identifier) - (const :tag "Sort by title" title) - (const :tag "Sort by keywords" keywords) - (const :tag "Sort by signature" signature)) - :package-version '(denote . "3.1.0") - :group 'denote-sort) - -(defcustom denote-sort-dired-default-reverse-sort nil - "If non-nil, reverse the sorting order by default. -This is used only if `denote-sort-dired-extra-prompts' omits the -minibuffer prompt that asks for a reverse sort or not." - :type 'boolean - :package-version '(denote . "3.1.0") - :group 'denote-sort) - -;; NOTE 2023-12-04: We can have compound sorting algorithms such as -;; title+signature, but I want to keep this simple for the time being. -;; Let us first hear from users to understand if there is a real need -;; for such a feature. -(defmacro denote-sort--define-lessp (component) - "Define function to sort by COMPONENT." - (let ((retrieve-fn (intern (format "denote-retrieve-filename-%s" component))) - (comparison-fn (intern (format "denote-sort-%s-comparison-function" component)))) - `(defun ,(intern (format "denote-sort-%s-lessp" component)) (file1 file2) - ,(format - "Return smallest among FILE1, FILE2 based on their %s. -The `%s' performs the comparison." - component comparison-fn) - (let* ((one (,retrieve-fn file1)) - (two (,retrieve-fn file2)) - (one-empty-p (or (null one) (string-empty-p one))) - (two-empty-p (or (null two) (string-empty-p two)))) - (cond - (one-empty-p nil) - ((and (not one-empty-p) two-empty-p) one) - (t (funcall (or ,comparison-fn denote-sort-comparison-fallback-function) one two))))))) - -;; TODO 2023-12-04: Subject to the above NOTE, we can also sort by -;; directory and by file length. -(denote-sort--define-lessp identifier) -(denote-sort--define-lessp title) -(denote-sort--define-lessp keywords) -(denote-sort--define-lessp signature) - -;;;###autoload -(defun denote-sort-files (files component &optional reverse) - "Returned sorted list of Denote FILES. - -With COMPONENT as a symbol among `denote-sort-components', -sort files based on the corresponding file name component. - -With COMPONENT as the symbol of a function, use it to perform the -sorting. In this case, the function is called with two arguments, as -described by `sort'. - -With COMPONENT as a nil value keep the original date-based -sorting which relies on the identifier of each file name. - -With optional REVERSE as a non-nil value, reverse the sort order." - (let* ((files-to-sort (copy-sequence files)) - (sort-fn (pcase component - ((pred functionp) component) - ('identifier #'denote-sort-identifier-lessp) - ('title #'denote-sort-title-lessp) - ('keywords #'denote-sort-keywords-lessp) - ('signature #'denote-sort-signature-lessp))) - (sorted-files (if sort-fn (sort files sort-fn) files-to-sort))) - (if reverse - (reverse sorted-files) - sorted-files))) - -(defun denote-sort-get-directory-files (files-matching-regexp sort-by-component &optional reverse omit-current exclude-regexp) - "Return sorted list of files in variable `denote-directory'. - -With FILES-MATCHING-REGEXP as a string limit files to those -matching the given regular expression. - -With SORT-BY-COMPONENT as a symbol among `denote-sort-components', -pass it to `denote-sort-files' to sort by the corresponding file -name component. - -With optional REVERSE as a non-nil value, reverse the sort order. - -With optional OMIT-CURRENT, do not include the current file in -the list. - -With optional EXCLUDE-REGEXP exclude the files that match the given -regular expression. This is done after FILES-MATCHING-REGEXP and -OMIT-CURRENT have been applied." - (denote-sort-files - (denote-directory-files files-matching-regexp omit-current nil exclude-regexp) - sort-by-component - reverse)) - -(defun denote-sort-get-links (files-matching-regexp sort-by-component current-file-type id-only &optional reverse exclude-regexp) - "Return sorted typographic list of links for FILES-MATCHING-REGEXP. - -With FILES-MATCHING-REGEXP as a string, match files stored in the -variable `denote-directory'. - -With SORT-BY-COMPONENT as a symbol among `denote-sort-components', -sort FILES-MATCHING-REGEXP by the given Denote file name -component. If SORT-BY-COMPONENT is nil or an unknown non-nil -value, default to the identifier-based sorting. - -With CURRENT-FILE-TYPE as a symbol among those specified in -the variable `denote-file-type' (or the `car' of each element in -`denote-file-types'), format the link accordingly. With a nil or -unknown non-nil value, default to the Org notation. - -With ID-ONLY as a non-nil value, produce links that consist only -of the identifier, thus deviating from CURRENT-FILE-TYPE. - -With optional REVERSE as a non-nil value, reverse the sort order. - -With optional EXCLUDE-REGEXP exclude the files that match the given -regular expression. This is done after FILES-MATCHING-REGEXP and -OMIT-CURRENT have been applied." - (denote-link--prepare-links - (denote-sort-get-directory-files files-matching-regexp sort-by-component reverse exclude-regexp) - current-file-type - id-only)) - -(defvar denote-sort-component-history nil - "Minibuffer history of `denote-sort-component-prompt'.") - -(defalias 'denote-sort--component-hist 'denote-sort-component-history - "Compatibility alias for `denote-sort-component-history'.") - -(defun denote-sort-component-prompt () - "Prompt for sorting key among `denote-sort-components'." - (let ((default (car denote-sort-component-history))) - (intern - (completing-read - (format-prompt "Sort by file name component" default) - denote-sort-components nil :require-match - nil 'denote-sort-component-history default)))) - -(defvar denote-sort-exclude-files-history nil - "Minibuffer history for `denote-sort-exclude-files-prompt'.") - -(defun denote-sort-exclude-files-prompt () - "Prompt for regular expression of files to exclude." - ;; TODO 2024-12-03: Maybe use `read-regexp'? We do not use it - ;; elsewhere, so maybe this is fine. - (let ((default (car denote-sort-exclude-files-history))) - (read-string - (format-prompt "Exclude files matching REGEXP" default) - default 'denote-sort-exclude-files-history))) - -(defvar-local denote-sort--dired-buffer nil - "Buffer object of current `denote-sort-dired'.") - -(defun denote-sort-dired--prompts () - "Return list of prompts per `denote-sort-dired-extra-prompts'." - (let (sort-by-component reverse-sort exclude-rx) - (dolist (prompt denote-sort-dired-extra-prompts) - (pcase prompt - ('sort-by-component (setq sort-by-component (denote-sort-component-prompt))) - ('reverse-sort (setq reverse-sort (y-or-n-p "Reverse sort? "))) - ('exclude-regexp (setq exclude-rx (denote-sort-exclude-files-prompt))))) - (list sort-by-component reverse-sort exclude-rx))) - -;;;###autoload -(defun denote-sort-dired (files-matching-regexp sort-by-component reverse exclude-regexp) - "Produce Dired buffer with sorted files from variable `denote-directory'. -When called interactively, prompt for FILES-MATCHING-REGEXP and, -depending on the value of the user option `denote-sort-dired-extra-prompts', -also prompt for SORT-BY-COMPONENT, REVERSE, and EXCLUDE-REGEXP. - -1. FILES-MATCHING-REGEXP limits the list of Denote files to - those matching the provided regular expression. - -2. SORT-BY-COMPONENT sorts the files by their file name component (one - among `denote-sort-components'). If it is nil, sorting is performed - according to the user option `denote-sort-dired-default-sort-component', - falling back to the identifier. - -3. REVERSE is a boolean to reverse the order when it is a non-nil value. - If `denote-sort-dired-extra-prompts' is configured to skip this - prompt, then the sorting is done according to the user option - `denote-sort-dired-default-reverse-sort', falling back to - nil (i.e. no reverse sort). - -4. EXCLUDE-REGEXP excludes the files that match the given regular - expression. This is done after FILES-MATCHING-REGEXP and - OMIT-CURRENT have been applied. - -When called from Lisp, the arguments are a string, a symbol among -`denote-sort-components', and a non-nil value, respectively." - (interactive - (append (list (denote-files-matching-regexp-prompt)) (denote-sort-dired--prompts))) - (let ((component (or sort-by-component - denote-sort-dired-default-sort-component - 'identifier)) - (reverse-sort (or reverse - denote-sort-dired-default-reverse-sort - nil)) - (exclude-rx (or exclude-regexp nil))) - (if-let* ((default-directory (denote-directory)) - (files (denote-sort-get-directory-files files-matching-regexp component reverse-sort nil exclude-rx)) - ;; NOTE 2023-12-04: Passing the FILES-MATCHING-REGEXP as - ;; buffer-name produces an error if the regexp contains a - ;; wildcard for a directory. I can reproduce this in emacs - ;; -Q and am not sure if it is a bug. Anyway, I will report - ;; it upstream, but even if it is fixed we cannot use it - ;; for now (whatever fix will be available for Emacs 30+). - (denote-sort-dired-buffer-name (format "Denote sort `%s' by `%s'" files-matching-regexp component)) - (buffer-name (format "Denote sort by `%s' at %s" component (format-time-string "%T")))) - (let ((dired-buffer (dired (cons buffer-name (mapcar #'file-relative-name files))))) - (setq denote-sort--dired-buffer dired-buffer) - (with-current-buffer dired-buffer - (setq-local revert-buffer-function - (lambda (&rest _) - ;; FIXME 2025-01-04: Killing the buffer has - ;; the unintended side effect of affecting the - ;; window configuration when we call - ;; `denote-update-dired-buffers'. - (kill-buffer dired-buffer) - (denote-sort-dired files-matching-regexp component reverse-sort exclude-rx)))) - ;; Because of the above NOTE, I am printing a message. Not - ;; what I want, but it is better than nothing... - (message denote-sort-dired-buffer-name)) - (message "No matching files for: %s" files-matching-regexp)))) - -(provide 'denote-sort) -;;; denote-sort.el ends here diff --git a/denote.el b/denote.el index f615235497..b763415d5f 100644 --- a/denote.el +++ b/denote.el @@ -1464,6 +1464,312 @@ Return the absolute path to the matching file." ;; appropriately. absolute-file)) +;;;; The sort mechanism + +(defgroup denote-sort nil + "Sort Denote files based on a file name component." + :group 'denote + :link '(info-link "(denote) Top") + :link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote")) + +(defconst denote-sort-comparison-fallback-function #'string-collate-lessp + "String comparison function used by `denote-sort-files' subroutines.") + +(defconst denote-sort-components '(title keywords signature identifier) + "List of sorting keys applicable for `denote-sort-files' and related.") + +(defcustom denote-sort-identifier-comparison-function denote-sort-comparison-fallback-function + "Function to sort the DATE/IDENTIFIER component in file names. +The function accepts two arguments and must return a non-nil value if +the first argument is smaller than the second one." + :type 'function + :package-version '(denote . "3.2.0") + :group 'denote-sort) + +(defcustom denote-sort-title-comparison-function denote-sort-comparison-fallback-function + "Function to sort the TITLE component in file names. +The function accepts two arguments and must return a non-nil value if +the first argument is smaller than the second one." + :type 'function + :package-version '(denote . "3.1.0") + :group 'denote-sort) + +(defcustom denote-sort-keywords-comparison-function denote-sort-comparison-fallback-function + "Function to sort the KEYWORDS component in file names. +The function accepts two arguments and must return a non-nil value if +the first argument is smaller than the second one." + :type 'function + :package-version '(denote . "3.1.0") + :group 'denote-sort) + +(defcustom denote-sort-signature-comparison-function denote-sort-comparison-fallback-function + "Function to sort the SIGNATURE component in file names. +The function accepts two arguments and must return a non-nil value if +the first argument is smaller than the second one." + :type 'function + :package-version '(denote . "3.1.0") + :group 'denote-sort) + +(defcustom denote-sort-dired-extra-prompts '(sort-by-component reverse-sort) + "Determine what `denote-sort-dired' prompts for beside a search query. +This concerns the additional prompts issued by `denote-sort-dired' about +whether to sort by a given file name component and to then reverse the +sort. + +The value is a list of symbols, which can include the symbols +`sort-by-component', `reverse-sort', and `exclude-regexp'. The order is +significant, with the leftmost symbol coming first. + +These symbols correspond to the following: + +- A choice to select the file name component to sort by. +- A yes or no prompt on whether to reverse the sorting. +- A string (or regular expression) of files to be excluded from the results. + +If the value is nil, skip all prompts. In this scenario, the sorting is +done according to `denote-sort-dired-default-sort-component' and +`denote-sort-dired-default-reverse-sort'." + :type '(radio (const :tag "Do not prompt for anything" nil) + (set :tag "Available prompts" :greedy t + (const :tag "Sort by file name component" sort-by-component) + (const :tag "Reverse the sort" reverse-sort) + (const :tag "Exclude files matching regexp" exclude-regexp))) + :package-version '(denote . "3.2.0") + :group 'denote-sort) + +(defcustom denote-sort-dired-default-sort-component 'identifier + "Set the default file name component to sort by. +This is used only if `denote-sort-dired-extra-prompts' omits the +minibuffer prompt for which file name component to sort by." + :type '(radio + (const :tag "Sort by identifier (default)" identifier) + (const :tag "Sort by title" title) + (const :tag "Sort by keywords" keywords) + (const :tag "Sort by signature" signature)) + :package-version '(denote . "3.1.0") + :group 'denote-sort) + +(defcustom denote-sort-dired-default-reverse-sort nil + "If non-nil, reverse the sorting order by default. +This is used only if `denote-sort-dired-extra-prompts' omits the +minibuffer prompt that asks for a reverse sort or not." + :type 'boolean + :package-version '(denote . "3.1.0") + :group 'denote-sort) + +;; NOTE 2023-12-04: We can have compound sorting algorithms such as +;; title+signature, but I want to keep this simple for the time being. +;; Let us first hear from users to understand if there is a real need +;; for such a feature. +(defmacro denote-sort--define-lessp (component) + "Define function to sort by COMPONENT." + (let ((retrieve-fn (intern (format "denote-retrieve-filename-%s" component))) + (comparison-fn (intern (format "denote-sort-%s-comparison-function" component)))) + `(defun ,(intern (format "denote-sort-%s-lessp" component)) (file1 file2) + ,(format + "Return smallest among FILE1, FILE2 based on their %s. +The `%s' performs the comparison." + component comparison-fn) + (let* ((one (,retrieve-fn file1)) + (two (,retrieve-fn file2)) + (one-empty-p (or (null one) (string-empty-p one))) + (two-empty-p (or (null two) (string-empty-p two)))) + (cond + (one-empty-p nil) + ((and (not one-empty-p) two-empty-p) one) + (t (funcall (or ,comparison-fn denote-sort-comparison-fallback-function) one two))))))) + +;; TODO 2023-12-04: Subject to the above NOTE, we can also sort by +;; directory and by file length. +(denote-sort--define-lessp identifier) +(denote-sort--define-lessp title) +(denote-sort--define-lessp keywords) +(denote-sort--define-lessp signature) + +;;;###autoload +(defun denote-sort-files (files component &optional reverse) + "Returned sorted list of Denote FILES. + +With COMPONENT as a symbol among `denote-sort-components', +sort files based on the corresponding file name component. + +With COMPONENT as the symbol of a function, use it to perform the +sorting. In this case, the function is called with two arguments, as +described by `sort'. + +With COMPONENT as a nil value keep the original date-based +sorting which relies on the identifier of each file name. + +With optional REVERSE as a non-nil value, reverse the sort order." + (let* ((files-to-sort (copy-sequence files)) + (sort-fn (pcase component + ((pred functionp) component) + ('identifier #'denote-sort-identifier-lessp) + ('title #'denote-sort-title-lessp) + ('keywords #'denote-sort-keywords-lessp) + ('signature #'denote-sort-signature-lessp))) + (sorted-files (if sort-fn (sort files sort-fn) files-to-sort))) + (if reverse + (reverse sorted-files) + sorted-files))) + +(defun denote-sort-get-directory-files (files-matching-regexp sort-by-component &optional reverse omit-current exclude-regexp) + "Return sorted list of files in variable `denote-directory'. + +With FILES-MATCHING-REGEXP as a string limit files to those +matching the given regular expression. + +With SORT-BY-COMPONENT as a symbol among `denote-sort-components', +pass it to `denote-sort-files' to sort by the corresponding file +name component. + +With optional REVERSE as a non-nil value, reverse the sort order. + +With optional OMIT-CURRENT, do not include the current file in +the list. + +With optional EXCLUDE-REGEXP exclude the files that match the given +regular expression. This is done after FILES-MATCHING-REGEXP and +OMIT-CURRENT have been applied." + (denote-sort-files + (denote-directory-files files-matching-regexp omit-current nil exclude-regexp) + sort-by-component + reverse)) + +(defun denote-sort-get-links (files-matching-regexp sort-by-component current-file-type id-only &optional reverse exclude-regexp) + "Return sorted typographic list of links for FILES-MATCHING-REGEXP. + +With FILES-MATCHING-REGEXP as a string, match files stored in the +variable `denote-directory'. + +With SORT-BY-COMPONENT as a symbol among `denote-sort-components', +sort FILES-MATCHING-REGEXP by the given Denote file name +component. If SORT-BY-COMPONENT is nil or an unknown non-nil +value, default to the identifier-based sorting. + +With CURRENT-FILE-TYPE as a symbol among those specified in +the variable `denote-file-type' (or the `car' of each element in +`denote-file-types'), format the link accordingly. With a nil or +unknown non-nil value, default to the Org notation. + +With ID-ONLY as a non-nil value, produce links that consist only +of the identifier, thus deviating from CURRENT-FILE-TYPE. + +With optional REVERSE as a non-nil value, reverse the sort order. + +With optional EXCLUDE-REGEXP exclude the files that match the given +regular expression. This is done after FILES-MATCHING-REGEXP and +OMIT-CURRENT have been applied." + (denote-link--prepare-links + (denote-sort-get-directory-files files-matching-regexp sort-by-component reverse exclude-regexp) + current-file-type + id-only)) + +(defvar denote-sort-component-history nil + "Minibuffer history of `denote-sort-component-prompt'.") + +(defalias 'denote-sort--component-hist 'denote-sort-component-history + "Compatibility alias for `denote-sort-component-history'.") + +(defun denote-sort-component-prompt () + "Prompt for sorting key among `denote-sort-components'." + (let ((default (car denote-sort-component-history))) + (intern + (completing-read + (format-prompt "Sort by file name component" default) + denote-sort-components nil :require-match + nil 'denote-sort-component-history default)))) + +(defvar denote-sort-exclude-files-history nil + "Minibuffer history for `denote-sort-exclude-files-prompt'.") + +(defun denote-sort-exclude-files-prompt () + "Prompt for regular expression of files to exclude." + ;; TODO 2024-12-03: Maybe use `read-regexp'? We do not use it + ;; elsewhere, so maybe this is fine. + (let ((default (car denote-sort-exclude-files-history))) + (read-string + (format-prompt "Exclude files matching REGEXP" default) + default 'denote-sort-exclude-files-history))) + +(defvar-local denote-sort--dired-buffer nil + "Buffer object of current `denote-sort-dired'.") + +(defun denote-sort-dired--prompts () + "Return list of prompts per `denote-sort-dired-extra-prompts'." + (let (sort-by-component reverse-sort exclude-rx) + (dolist (prompt denote-sort-dired-extra-prompts) + (pcase prompt + ('sort-by-component (setq sort-by-component (denote-sort-component-prompt))) + ('reverse-sort (setq reverse-sort (y-or-n-p "Reverse sort? "))) + ('exclude-regexp (setq exclude-rx (denote-sort-exclude-files-prompt))))) + (list sort-by-component reverse-sort exclude-rx))) + +;;;###autoload +(defun denote-sort-dired (files-matching-regexp sort-by-component reverse exclude-regexp) + "Produce Dired buffer with sorted files from variable `denote-directory'. +When called interactively, prompt for FILES-MATCHING-REGEXP and, +depending on the value of the user option `denote-sort-dired-extra-prompts', +also prompt for SORT-BY-COMPONENT, REVERSE, and EXCLUDE-REGEXP. + +1. FILES-MATCHING-REGEXP limits the list of Denote files to + those matching the provided regular expression. + +2. SORT-BY-COMPONENT sorts the files by their file name component (one + among `denote-sort-components'). If it is nil, sorting is performed + according to the user option `denote-sort-dired-default-sort-component', + falling back to the identifier. + +3. REVERSE is a boolean to reverse the order when it is a non-nil value. + If `denote-sort-dired-extra-prompts' is configured to skip this + prompt, then the sorting is done according to the user option + `denote-sort-dired-default-reverse-sort', falling back to + nil (i.e. no reverse sort). + +4. EXCLUDE-REGEXP excludes the files that match the given regular + expression. This is done after FILES-MATCHING-REGEXP and + OMIT-CURRENT have been applied. + +When called from Lisp, the arguments are a string, a symbol among +`denote-sort-components', and a non-nil value, respectively." + (interactive + (append (list (denote-files-matching-regexp-prompt)) (denote-sort-dired--prompts))) + (let ((component (or sort-by-component + denote-sort-dired-default-sort-component + 'identifier)) + (reverse-sort (or reverse + denote-sort-dired-default-reverse-sort + nil)) + (exclude-rx (or exclude-regexp nil))) + (if-let* ((default-directory (denote-directory)) + (files (denote-sort-get-directory-files files-matching-regexp component reverse-sort nil exclude-rx)) + ;; NOTE 2023-12-04: Passing the FILES-MATCHING-REGEXP as + ;; buffer-name produces an error if the regexp contains a + ;; wildcard for a directory. I can reproduce this in emacs + ;; -Q and am not sure if it is a bug. Anyway, I will report + ;; it upstream, but even if it is fixed we cannot use it + ;; for now (whatever fix will be available for Emacs 30+). + (denote-sort-dired-buffer-name (format "Denote sort `%s' by `%s'" files-matching-regexp component)) + (buffer-name (format "Denote sort by `%s' at %s" component (format-time-string "%T")))) + (let ((dired-buffer (dired (cons buffer-name (mapcar #'file-relative-name files))))) + (setq denote-sort--dired-buffer dired-buffer) + (with-current-buffer dired-buffer + (setq-local revert-buffer-function + (lambda (&rest _) + ;; FIXME 2025-01-04: Killing the buffer has + ;; the unintended side effect of affecting the + ;; window configuration when we call + ;; `denote-update-dired-buffers'. + (kill-buffer dired-buffer) + (denote-sort-dired files-matching-regexp component reverse-sort exclude-rx)))) + ;; Because of the above NOTE, I am printing a message. Not + ;; what I want, but it is better than nothing... + (message denote-sort-dired-buffer-name)) + (message "No matching files for: %s" files-matching-regexp)))) + +(defalias 'denote-sort-dired 'denote-dired + "Alias for `denote-sort-dired' command.") + ;;;; Keywords (defun denote-extract-keywords-from-path (path)