branch: externals/denote
commit a4d50fbf7131a5a92bcf4b54446248296412829f
Author: Jean-Philippe Gagné Guay <jeanphilippe...@gmail.com>
Commit: Jean-Philippe Gagné Guay <jeanphilippe...@gmail.com>

    Make identifier and date independent
---
 README.org           |  69 +++++-----
 denote.el            | 378 +++++++++++++++++++++++++++++++++++----------------
 tests/denote-test.el |  14 +-
 3 files changed, 300 insertions(+), 161 deletions(-)

diff --git a/README.org b/README.org
index a7bce339cc..8fd51eaf3f 100644
--- a/README.org
+++ b/README.org
@@ -430,6 +430,10 @@ of the following:
 
   [[#h:e7ef08d6-af1b-4ab3-bb00-494a653e6d63][The 
denote-date-prompt-use-org-read-date option]].
 
+- =identifier=: Prompts with completion for the identifier of the new
+  note. It expects a string that has the format of
+  ~denote-date-identifier-format~.
+
 - =template=: Prompts for a KEY among the ~denote-templates~.  The value
   of that KEY is used to populate the new note with content, which is
   added after the front matter ([[#h:f635a490-d29e-4608-9372-7bd13b34d56c][The 
denote-templates option]]).
@@ -452,15 +456,15 @@ time) and a supported file type extension (per 
~denote-file-type~).
 Recall that Denote's standard file-naming scheme is defined as follows
 ([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file-naming scheme]]):
 
-: DATE--TITLE__KEYWORDS.EXT
+: ID--TITLE__KEYWORDS.EXT
 
 If either or both of the =title= and =keywords= prompts are not
 included in the value of this variable, file names will be any of
 those permutations:
 
-: DATE.EXT
-: DATE--TITLE.EXT
-: DATE__KEYWORDS.EXT
+: ID.EXT
+: ID--TITLE.EXT
+: ID__KEYWORDS.EXT
 
 When in doubt, always include the =title= and =keywords= prompts.
 
@@ -2024,13 +2028,14 @@ directories.
 Every note produced by Denote follows this pattern by default
 ([[#h:17896c8c-d97a-4faa-abf6-31df99746ca6][Points of entry]]):
 
-: DATE==SIGNATURE--TITLE__KEYWORDS.EXTENSION
+: ID==SIGNATURE--TITLE__KEYWORDS.EXTENSION
 
-The =DATE= field represents the date in year-month-day format followed
-by the capital letter =T= (for "time") and the current time in
-hour-minute-second notation.  The presentation is compact:
-=20220531T091625=.  The =DATE= serves as the unique identifier of each
-note and, as such, is also known as the file's ID or identifier.
+The =ID= field represents the identifier which, by default, is the
+date in year-month-day format followed by the capital letter =T= (for
+"time") and the current time in hour-minute-second notation. The
+presentation is compact: =20220531T091625=. The =ID= serves as the
+unique identifier of each note and, as such, is also known as the
+file's ID or identifier.
 
 File names can include an arbitrary string of alphanumeric characters
 in the =SIGNATURE= field. Signatures have no clearly defined purpose
@@ -2090,13 +2095,13 @@ invoking =M-x re-builder=).
 The ~denote-prompts~ can be configured in such ways to yield the
 following file name permutations:
 
-: DATE.EXT
-: DATE--TITLE.EXT
-: DATE__KEYWORDS.EXT
-: DATE==SIGNATURE.EXT
-: DATE==SIGNATURE--TITLE.EXT
-: DATE==SIGNATURE--TITLE__KEYWORDS.EXT
-: DATE==SIGNATURE__KEYWORDS.EXT
+: ID.EXT
+: ID--TITLE.EXT
+: ID__KEYWORDS.EXT
+: ID==SIGNATURE.EXT
+: ID==SIGNATURE--TITLE.EXT
+: ID==SIGNATURE--TITLE__KEYWORDS.EXT
+: ID==SIGNATURE__KEYWORDS.EXT
 
 When in doubt, stick to the default design, which is carefully
 considered and works well ([[#h:dc8c40e0-233a-4991-9ad3-2cf5f05ef1cd][Change 
the order of file name components]]).
@@ -2356,11 +2361,11 @@ use the following in their Emacs configuration 
([[#h:d375c6d2-92c7-425f-9d9d-219
 By default, file names have three fields and two sets of field
 delimiters between them:
 
-: DATE--TITLE__KEYWORDS.EXTENSION
+: ID--TITLE__KEYWORDS.EXTENSION
 
 When a signature is present, this becomes:
 
-: DATE==SIGNATURE--TITLE__KEYWORDS.EXTENSION
+: ID==SIGNATURE--TITLE__KEYWORDS.EXTENSION
 
 Field delimiters practically serve as anchors for easier searching.
 Consider this example:
@@ -4970,9 +4975,9 @@ The following sections cover the specifics.
 + Variable ~denote-keywords-regexp~ :: Regular expression to match the
   =KEYWORDS= field in a file name 
([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file-naming scheme]]).
 
-#+findex: denote-identifier-p
-+ Function ~denote-identifier-p~ :: Return non-nil if =IDENTIFIER=
-  string is a Denote identifier.
+#+findex: denote-date-identifier-p
++ Function ~denote-date-identifier-p~ :: Return non-nil if =IDENTIFIER=
+  string is a Denote date identifier.
 
 #+findex: denote-file-is-note-p
 + Function ~denote-file-is-note-p~ :: Return non-nil if =FILE= is an
@@ -5256,7 +5261,8 @@ there will be no corresponding prompt.
 #+findex: denote-retrieve-filename-identifier
 + Function ~denote-retrieve-filename-identifier~ :: Extract identifier
   from =FILE= name, if present, else return nil. To create a new one
-  from a date, refer to the ~denote-get-identifier~ function.
+  from a date, refer to the ~denote--identifier-generation-function~
+  variable.
 
 #+findex: denote-retrieve-filename-title
 + Function ~denote-retrieve-filename-title~ ::  Extract Denote title
@@ -5277,11 +5283,6 @@ there will be no corresponding prompt.
   ~denote-retrieve-front-matter-title-value~ and
   =denote-retrieve-filename-title=.
 
-#+findex: denote-get-identifier
-+ Function ~denote-get-identifier~ :: Convert =DATE= into a Denote
-  identifier using ~denote-date-identifier-format~. If =DATE= is nil, return an
-  empty string as the identifier.
-
 #+findex: denote-retrieve-front-matter-title-value
 + Function ~denote-retrieve-front-matter-title-value~ :: Return title value 
from
   =FILE= front matter per =FILE-TYPE=.
@@ -5310,6 +5311,14 @@ there will be no corresponding prompt.
   wrapper function around ~denote~, ~denote-rename-file~, and
   generally any command that consults the value of ~denote-prompts~.
 
+#+findex: denote-identifier-prompt
++ Function ~denote-identifier-prompt~ :: Prompt for identifier string.
+  With optional =INITIAL-IDENTIFIER= use it as the initial minibuffer
+  text. With optional =PROMPT-TEXT= use it in the minibuffer instead
+  of the default prompt. Previous inputs at this prompt are available
+  for minibuffer completion if the user option 
~denote-history-completion-in-prompts~
+  is set to a non-nil value ([[#h:403422a7-7578-494b-8f33-813874c12da3][The 
~denote-history-completion-in-prompts~ option]]).
+
 #+findex: denote-signature-prompt
 + Function ~denote-signature-prompt~ :: Prompt for signature string.
   With optional =INITIAL-SIGNATURE= use it as the initial minibuffer
@@ -5386,10 +5395,6 @@ there will be no corresponding prompt.
   =REGEXP= to filter Denote files by. With optional =PROMPT-TEXT= use
   it instead of a generic prompt.
 
-#+findex: denote-prompt-for-date-return-id
-+ Function ~denote-prompt-for-date-return-id~ :: Use
-  ~denote-date-prompt~ and return it as ~denote-date-identifier-format~.
-
 #+findex: denote-template-prompt
 + Function ~denote-template-prompt~ :: Prompt for template key in
   ~denote-templates~ and return its value.
diff --git a/denote.el b/denote.el
index c83547de9d..b56ea5bc99 100644
--- a/denote.el
+++ b/denote.el
@@ -269,6 +269,9 @@ of the following:
   leverage the more sophisticated Org method, see the
   `denote-date-prompt-use-org-read-date'.)
 
+- `identifier': Prompts for the identifier of the new note.  It expects
+  a string that has the format of `denote-date-identifier-format'.
+
 - `template': Prompts for a KEY among `denote-templates'.  The value of
   that KEY is used to populate the new note with content, which is added
   after the front matter.
@@ -292,19 +295,19 @@ time) and a supported file type extension (per the 
variable
 Recall that Denote's standard file-naming scheme is defined as
 follows (read the manual for the technicalities):
 
-    DATE--TITLE__KEYWORDS.EXT
+    ID--TITLE__KEYWORDS.EXT
 
 Depending on the inclusion of the `title', `keywords', and
 `signature' prompts, file names will be any of those
 permutations:
 
-    DATE.EXT
-    DATE--TITLE.EXT
-    DATE__KEYWORDS.EXT
-    DATE==SIGNATURE.EXT
-    DATE==SIGNATURE--TITLE.EXT
-    DATE==SIGNATURE--TITLE__KEYWORDS.EXT
-    DATE==SIGNATURE__KEYWORDS.EXT
+    ID.EXT
+    ID--TITLE.EXT
+    ID__KEYWORDS.EXT
+    ID==SIGNATURE.EXT
+    ID==SIGNATURE--TITLE.EXT
+    ID==SIGNATURE--TITLE__KEYWORDS.EXT
+    ID==SIGNATURE__KEYWORDS.EXT
 
 When in doubt, always include the `title' and `keywords'
 prompts (the default style).
@@ -326,6 +329,7 @@ To change the order of the file name components, refer to
                      (const :tag "Title" title)
                      (const :tag "Keywords" keywords)
                      (const :tag "Date" date)
+                     (const :tag "Identifier" identifier)
                      (const :tag "File type extension" file-type)
                      (const :tag "Subdirectory" subdirectory)
                      (const :tag "Template" template)
@@ -1025,6 +1029,9 @@ IMPORTANT: Some features may not work with notes that do 
not have an
 identifier.  For example, backlinks do not contain files without an
 identifier.")
 
+(defvar denote-accept-nil-date nil
+  "Make creation and renaming commands use `current-time' when date is nil.")
+
 ;;;;; Sluggification functions
 
 (defun denote-slug-keep-only-ascii (str)
@@ -1065,6 +1072,18 @@ any leading and trailing signs."
     "=\\{2,\\}" "="
     (replace-regexp-in-string "_\\|\s+" "=" str))))
 
+(defun denote--valid-identifier (identifier)
+  "Ensure that IDENTIFIER is valid.
+
+It must not contain square brackets, parenthesis, \"query-filenames:\"
+or \"query-contents\"."
+  (replace-regexp-in-string
+   "query-filenames:" ""
+   (replace-regexp-in-string
+    "query-contents:" ""
+    (replace-regexp-in-string
+     "[][()]*" "" identifier))))
+
 (defun denote--remove-dot-characters (str)
   "Remove dot characters from STR."
   (replace-regexp-in-string "\\." "" str))
@@ -1110,6 +1129,8 @@ Also enforce the rules of the file-naming scheme."
                           (replace-regexp-in-string
                            "_" ""
                            (funcall (or slug-function 
#'denote-sluggify-keyword) str)))
+                         ((eq component 'identifier)
+                          (denote--valid-identifier str))
                          ((eq component 'signature)
                           (funcall (or slug-function 
#'denote-sluggify-signature) str)))))
     (denote--trim-right-token-characters
@@ -1151,10 +1172,15 @@ Also enforce the rules of the file-naming scheme."
   "Return non-nil if FILE is empty."
   (zerop (or (file-attribute-size (file-attributes file)) 0)))
 
-(defun denote-identifier-p (identifier)
-  "Return non-nil if IDENTIFIER string is a Denote identifier."
+(defun denote-date-identifier-p (identifier)
+  "Return non-nil if IDENTIFIER string is a Denote date identifier."
   (string-match-p (format "\\`%s\\'" denote-date-identifier-regexp) 
identifier))
 
+(make-obsolete
+ 'denote-identifier-p
+ 'denote-date-identifier-p
+ "4.1.0")
+
 (defun denote-file-has-identifier-p (file)
   "Return non-nil if FILE has a Denote identifier."
   (denote-retrieve-filename-identifier file))
@@ -2346,29 +2372,29 @@ COMPONENT can be one of `title', `keywords', 
`identifier', `date', `signature'."
     ('date #'denote--date-key-regexp)
     ('identifier #'denote--identifier-key-regexp)))
 
-(defun denote--format-front-matter (title date keywords id signature filetype)
+(defun denote--format-front-matter (title date keywords identifier signature 
filetype)
   "Front matter for new notes.
 
-TITLE, SIGNATURE, and ID are strings.  DATE is a date object.  KEYWORDS
+TITLE, SIGNATURE, and IDENTIFIER are strings.  DATE is a date object.  KEYWORDS
 is a list of strings.  FILETYPE is one of the values of variable
 `denote-file-type'."
   (let* ((fm (denote--front-matter filetype))
          (title-value-function (denote--title-value-function filetype))
          (keywords-value-function (denote--keywords-value-function filetype))
-         (id-value-function (denote--identifier-value-function filetype))
+         (identifier-value-function (denote--identifier-value-function 
filetype))
          (signature-value-function (denote--signature-value-function filetype))
          (title-string (if title-value-function (funcall title-value-function 
title) ""))
          (date-string (denote--format-front-matter-date date filetype))
          (keywords-string (if keywords-value-function (funcall 
keywords-value-function (denote-sluggify-keywords-and-apply-rules keywords)) 
""))
-         (id-string (if id-value-function (funcall id-value-function id) ""))
+         (identifier-string (if identifier-value-function (funcall 
identifier-value-function identifier) ""))
          (signature-string (if signature-value-function (funcall 
signature-value-function (denote-sluggify-and-apply-rules 'signature 
signature)) ""))
-         (new-front-matter (if fm (format fm title-string date-string 
keywords-string id-string signature-string) "")))
+         (new-front-matter (if fm (format fm title-string date-string 
keywords-string identifier-string signature-string) "")))
     ;; Remove lines with empty values if the corresponding component
     ;; is not in `denote-front-matter-components-present-even-if-empty-value'.
     (with-temp-buffer
       (insert new-front-matter)
       (dolist (component '(title date keywords signature identifier))
-        (let ((value (pcase component ('title title) ('keywords keywords) 
('signature signature) ('date date) ('identifier id)))
+        (let ((value (pcase component ('title title) ('keywords keywords) 
('signature signature) ('date date) ('identifier identifier)))
               (component-key-regexp-function 
(denote--get-component-key-regexp-function component)))
           (goto-char (point-min))
           (when (and (not (denote--component-has-value-p component value))
@@ -2383,8 +2409,8 @@ is a list of strings.  FILETYPE is one of the values of 
variable
 (defun denote-retrieve-filename-identifier (file)
   "Extract identifier from FILE name, if present, else return nil.
 
-To create a new one from a date, refer to the function
-`denote-get-identifier'."
+To create a new one from a date, refer to the function referred by
+`denote--identifier-generation-function'."
   (let ((filename (file-name-nondirectory file)))
     (cond ((string-match (concat "\\`" denote-date-identifier-regexp) filename)
            (match-string-no-properties 0 filename))
@@ -2398,13 +2424,6 @@ To create a new one from a date, refer to the function
   (or (denote-retrieve-filename-identifier file)
       (error "Cannot find `%s' as a file with a Denote identifier" file)))
 
-(defun denote-get-identifier (date)
-  "Convert DATE into a Denote identifier using `denote-date-identifier-format'.
-If DATE is nil, return an empty string as the identifier."
-  (if date
-      (format-time-string denote-date-identifier-format date)
-    ""))
-
 (defvar denote--used-ids nil
   "Hash table of used identifiers.
 This variable should be set only for the duration of a command.
@@ -2660,7 +2679,7 @@ which case it is not added to the base file name."
                                '(identifier signature title keywords))))
     (dolist (component components)
       (cond ((and (eq component 'identifier) id (not (string-empty-p id)))
-             (setq file-name (concat file-name "@@" id)))
+             (setq file-name (concat file-name "@@" (denote-sluggify 
'identifier id))))
             ((and (eq component 'title) title (not (string-empty-p title)))
              (setq file-name (concat file-name "--" 
(denote-sluggify-and-apply-rules 'title title))))
             ((and (eq component 'keywords) keywords)
@@ -2794,7 +2813,7 @@ If DATE is nil or an empty string, return nil."
 
 (defun denote-id-to-date (identifier)
   "Convert IDENTIFIER string to YYYY-MM-DD."
-  (if (denote-identifier-p identifier)
+  (if (denote-date-identifier-p identifier)
       (replace-regexp-in-string
        "\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\).*"
        "\\1-\\2-\\3"
@@ -2825,21 +2844,71 @@ It checks files in variable `denote-directory' and 
active buffer files."
         (puthash id t ids)))
     ids))
 
-(defun denote--find-first-unused-id (id)
+(defun denote--find-first-unused-id-as-date (id)
   "Return the first unused id starting at ID.
 If ID is already used, increment it 1 second at a time until an
 available id is found."
-  (let ((used-ids (or denote--used-ids (denote--get-all-used-ids)))
-        (current-id id)
+  (let ((current-id id)
         (iteration 0))
-    (while (gethash current-id used-ids)
+    (while (gethash current-id denote--used-ids)
       ;; Prevent infinite loop if `denote-date-identifier-format' is 
misconfigured
       (setq iteration (1+ iteration))
       (when (>= iteration 10000)
         (user-error "A unique identifier could not be found"))
-      (setq current-id (denote-get-identifier (time-add (date-to-time 
current-id) 1))))
+      (setq current-id (format-time-string
+                        denote-date-identifier-format
+                        (time-add (date-to-time current-id) 1))))
+    current-id))
+
+(defun denote-generate-identifier-as-date (initial-identifier date)
+  "Generate an identifier based on DATE.
+
+If INITIAL-IDENTIFIER is not already used, return it.  Else, if it is
+possible to derive an identifier from it, return this identifier.
+
+Else, use the DATE.  If it is nil, use `current-time'."
+  (let ((denote--used-ids (or denote--used-ids (denote--get-all-used-ids))))
+    (cond ((and initial-identifier
+                (not (gethash initial-identifier denote--used-ids)))
+           initial-identifier)
+          ((and initial-identifier
+                (string-match-p denote-date-identifier-regexp 
initial-identifier)
+                (date-to-time initial-identifier))
+           (denote--find-first-unused-id-as-date initial-identifier))
+          (t
+           (denote--find-first-unused-id-as-date
+            (format-time-string denote-date-identifier-format (or date 
(current-time))))))))
+
+(defun denote--find-first-unused-id-as-number (id)
+  "Return the first unused id starting at ID.
+If ID is already used, increment it until an available id is found."
+  (let ((current-id id)
+        (iteration 0))
+    (while (gethash current-id denote--used-ids)
+      ;; Prevent infinite loop
+      (setq iteration (1+ iteration))
+      (when (>= iteration 10000)
+        (user-error "A unique identifier could not be found"))
+      (setq current-id (number-to-string (1+ (string-to-number current-id)))))
     current-id))
 
+(defun denote-generate-identifier-as-number (initial-identifier _date)
+  "Generate an increasing number identifier.
+
+If INITIAL-IDENTIFIER is not already used, return it.  Else, if it is
+possible to derive an identifier from it, return this identifier.
+
+Else, use the first unused number starting from 1."
+  (let ((denote--used-ids (or denote--used-ids (denote--get-all-used-ids))))
+    (cond ((and initial-identifier
+                (not (gethash initial-identifier denote--used-ids)))
+           initial-identifier)
+          ((and initial-identifier
+                (string-match-p "[1-9][0-9]*" initial-identifier))
+           (denote--find-first-unused-id-as-number initial-identifier))
+          (t
+           (denote--find-first-unused-id-as-number "1")))))
+
 (defvar denote-command-prompt-history nil
   "Minibuffer history for `denote-command-prompt'.")
 
@@ -2978,7 +3047,7 @@ and the keywords prompt will be skipped.")
 See the documentation of `denote' for acceptable values.  This variable
 is ignored if nil.
 
-Only ever `let' bind this, otherwise the signaturew will always be the same
+Only ever `let' bind this, otherwise the signature will always be the same
 and the signature prompt will be skipped.")
 
 (defvar denote-use-file-type nil
@@ -3005,6 +3074,14 @@ is ignored if nil.
 Only ever `let' bind this, otherwise the date will always be the same
 and the date prompt will be skipped.")
 
+(defvar denote-use-identifier nil
+  "The identifier to be used in a note creation command.
+See the documentation of `denote' for acceptable values.  This variable
+is ignored if nil.
+
+Only ever `let' bind this, otherwise the identifier will always be the same
+and the identifier prompt will be skipped.")
+
 (defvar denote-use-template nil
   "The template to be used in a note creation command.
 See the documentation of `denote' for acceptable values.  This variable
@@ -3024,7 +3101,7 @@ from `denote-use-*' variables.  For example, if
 `denote-use-title' is set to a title, then no prompts happen for
 the title and the value of `denote-use-title' will be used
 instead."
-  (let (title keywords file-type directory date template signature)
+  (let (title keywords file-type directory date identifier template signature)
     (dolist (prompt denote-prompts)
       (pcase prompt
         ('title (unless denote-use-title
@@ -3042,17 +3119,31 @@ instead."
                          (setq directory (denote-subdirectory-prompt))))
         ('date (unless denote-use-date
                  (setq date (denote-date-prompt))))
+        ('identifier (unless denote-use-identifier
+                       (setq identifier (denote-identifier-prompt))))
         ('template (unless denote-use-template
                      (setq template (denote-template-prompt))))
         ('signature (unless denote-use-signature
                       (setq signature (denote-signature-prompt))))))
-    (list title keywords file-type directory date template signature)))
+    (list title keywords file-type directory date identifier template 
signature)))
+
+(defvar denote--identifier-generation-function 
#'denote-generate-identifier-as-date
+  "The function to use to generate identifiers.
+
+This function should return an identifier.  It should accept a date
+argument that may be omitted if it is not necessary to generate the
+identifier.
+
+Note that this function should ensure that the identifier is not already
+used.  The variable `denote--used-ids' can be used for this check.
+
+A non-empty identifier should always be returned.")
 
-(defun denote--creation-prepare-note-data (title keywords file-type directory 
date template signature)
+(defun denote--creation-prepare-note-data (title keywords file-type directory 
date identifier template signature)
   "Return parameters in a valid form for file creation.
 
 The data is: TITLE, KEYWORDS, FILE-TYPE, DIRECTORY, DATE,
-TEMPLATE and SIGNATURE.  The identifier is also returned.
+IDENTIFIER, TEMPLATE and SIGNATURE.  The identifier is also returned.
 
 If a `denote-use-*' variable is set for a data, its value is used
 instead of that of the parameter."
@@ -3062,6 +3153,7 @@ instead of that of the parameter."
          (file-type (or denote-use-file-type file-type))
          (directory (or denote-use-directory directory))
          (date (or denote-use-date date))
+         (identifier (or denote-use-identifier identifier))
          (template (or denote-use-template template))
          (signature (or denote-use-signature signature))
          ;; Make the data valid
@@ -3070,12 +3162,15 @@ instead of that of the parameter."
          (keywords (denote-keywords-sort keywords))
          (date (denote-valid-date-p date))
          (date (cond (date date)
-                     ((or (eq denote-generate-identifier-automatically t)
-                          (eq denote-generate-identifier-automatically 
'on-creation))
-                      (current-time))))
-         (id (denote-get-identifier date))
-         (id (if (string-empty-p id) id (denote--find-first-unused-id id)))
-         (date (if (string-empty-p id) nil (date-to-time id)))
+                     (denote-accept-nil-date date)
+                     (t (current-time))))
+         (identifier (or identifier ""))
+         (identifier (cond ((not (string-empty-p identifier))
+                            (funcall denote--identifier-generation-function 
identifier nil))
+                           ((or (eq denote-generate-identifier-automatically t)
+                                (eq denote-generate-identifier-automatically 
'on-creation))
+                            (funcall denote--identifier-generation-function 
nil (or date (current-time))))
+                           (t "")))
          (directory (if (and directory (denote--dir-in-denote-directory-p 
directory))
                         (file-name-as-directory directory)
                       (car (denote-directories))))
@@ -3083,10 +3178,14 @@ instead of that of the parameter."
                        template
                      (or (alist-get template denote-templates) "")))
          (signature (or signature "")))
-    (list title keywords file-type directory date id template signature)))
+    ;; TODO: Remove this when ready to allow custom identifiers.
+    (unless (and (string-match-p denote-date-identifier-regexp identifier)
+                 (date-to-time identifier))
+      (user-error "The identifier must have the format of 
`denote-date-identifier-format'"))
+    (list title keywords file-type directory date identifier template 
signature)))
 
 ;;;###autoload
-(defun denote (&optional title keywords file-type directory date template 
signature)
+(defun denote (&optional title keywords file-type directory date template 
signature identifier)
   "Create a new note with the appropriate metadata and file name.
 
 Run the `denote-after-new-note-hook' after creating the new note and
@@ -3116,15 +3215,22 @@ When called from Lisp, all arguments are optional.
   and time like 2022-06-16 14:30.  A nil value or an empty string
   is interpreted as the `current-time'.
 
+- IDENTIFIER is a string identifying the note.  It should have the
+  format of the variable `denote-date-identifier-format', like
+  20220630T1430000.
+
 - TEMPLATE is a symbol which represents the key of a cons cell in
   the user option `denote-templates'.  The value of that key is
   inserted to the newly created buffer after the front matter.
 
-- SIGNATURE is a string or a function returning a string."
-  (interactive (denote--creation-get-note-data-from-prompts))
-  (pcase-let* ((`(,title ,keywords ,file-type ,directory ,date ,id ,template 
,signature)
-                (denote--creation-prepare-note-data title keywords file-type 
directory date template signature))
-               (note-path (denote--prepare-note title keywords date id 
directory file-type template signature)))
+- SIGNATURE is a string."
+  (interactive
+   (pcase-let* ((`(,title ,keywords ,file-type ,directory ,date ,identifier 
,template ,signature)
+                 (denote--creation-get-note-data-from-prompts)))
+     (list title keywords file-type directory date template signature 
identifier)))
+  (pcase-let* ((`(,title ,keywords ,file-type ,directory ,date ,identifier 
,template ,signature)
+                (denote--creation-prepare-note-data title keywords file-type 
directory date identifier template signature))
+               (note-path (denote--prepare-note title keywords date identifier 
directory file-type template signature)))
     (denote--keywords-add-to-history keywords)
     (setq denote-current-data
           (list
@@ -3133,7 +3239,7 @@ When called from Lisp, all arguments are optional.
            (cons 'signature signature)
            (cons 'directory directory)
            (cons 'date date)
-           (cons 'id id)
+           (cons 'id identifier)
            (cons 'file-type file-type)
            (cons 'template template)))
     (run-hooks 'denote-after-new-note-hook)
@@ -3248,6 +3354,11 @@ Optional INITIAL-DATE and PROMPT-TEXT have the same 
meaning as
    (denote-valid-date-p
     (denote-date-prompt (denote-valid-date-p initial-date) prompt-text))))
 
+(make-obsolete
+ 'denote-prompt-for-date-return-id
+ 'denote-identifier-prompt
+ "4.1.0")
+
 (defvar denote-subdirectory-history nil
   "Minibuffer history of `denote-subdirectory-prompt'.")
 
@@ -3918,10 +4029,10 @@ The purpose of this variable is to be `let' bound to 
nil by a caller of
 the command `denote-rename-file' or related.  This will have the effect
 of not rewriting the file's front matter.")
 
-(defun denote--rename-file (file title keywords signature date)
+(defun denote--rename-file (file title keywords signature date identifier)
   "Rename FILE according to the other parameters.
-Parameters TITLE, KEYWORDS, SIGNATURE and DATE are as described
-in `denote-rename-file' and are assumed to be valid (TITLE and
+Parameters TITLE, KEYWORDS, SIGNATURE, DATE, and IDENTIFIER are as
+described in `denote-rename-file' and are assumed to be valid (TITLE and
 SIGNATURE are strings, KEYWORDS is a list, etc.).
 
 This function only does the work necessary to rename a file
@@ -3937,18 +4048,32 @@ Respect `denote-rename-confirmations', 
`denote-save-buffers' and
          (keywords (denote-keywords-sort keywords))
          (directory (file-name-directory file))
          (extension (denote-get-file-extension file))
-         (date (or date (denote--generate-date-for-rename file)))
-         (old-id (or (denote-retrieve-filename-identifier file) ""))
-         (id (denote-get-identifier date))
-         (id (cond ((or (string-empty-p id) (string= old-id id))
-                    id)
-                   ((and (not (string-empty-p old-id)) 
(denote--file-has-backlinks-p file))
-                    (user-error "The date cannot be modified because the file 
has backlinks"))
-                   (t
-                    (denote--find-first-unused-id id))))
-         (date (if (string-empty-p id) nil (date-to-time id)))
-         (new-name (denote-format-file-name directory id keywords title 
extension signature))
+         (date (cond (date date)
+                     (denote-accept-nil-date date)
+                     (t (or (file-attribute-modification-time (file-attributes 
file))
+                            (current-time)))))
+         (old-identifier (or (denote-retrieve-filename-identifier file) ""))
+         ;; Handle empty id
+         (identifier (if (and (string-empty-p identifier)
+                              (or (eq denote-generate-identifier-automatically 
t)
+                                  (eq denote-generate-identifier-automatically 
'on-rename)))
+                         (funcall denote--identifier-generation-function
+                                  nil
+                                  (or date
+                                      (file-attribute-modification-time 
(file-attributes file))
+                                      (current-time)))
+                       identifier))
+         (identifier (cond ((string-empty-p identifier) identifier)
+                           ((string= old-identifier identifier) identifier)
+                           ((denote--file-has-backlinks-p file)
+                            (user-error "The identifier cannot be modified 
because the new identifier has backlinks"))
+                           (t (funcall denote--identifier-generation-function 
identifier nil))))
+         (new-name (denote-format-file-name directory identifier keywords 
title extension signature))
          (max-mini-window-height denote-rename-max-mini-window-height))
+    ;; TODO: Remove this when ready to allow custom identifiers.
+    (unless (and (string-match-p denote-date-identifier-regexp identifier)
+                 (date-to-time identifier))
+      (user-error "The identifier must have the format of 
`denote-date-identifier-format'"))
     (when (and (file-regular-p new-name)
                (not (string= (expand-file-name file) (expand-file-name 
new-name))))
       (user-error "The destination file `%s' already exists" new-name))
@@ -3960,11 +4085,11 @@ Respect `denote-rename-confirmations', 
`denote-save-buffers' and
                (denote-file-has-supported-extension-p file)
                (denote-file-is-writable-and-supported-p new-name))
       (if (denote--file-has-front-matter-p new-name file-type)
-          (denote-rewrite-front-matter new-name title keywords signature date 
id file-type)
+          (denote-rewrite-front-matter new-name title keywords signature date 
identifier file-type)
         (when (denote-add-front-matter-prompt new-name)
-          (denote-prepend-front-matter new-name title keywords signature date 
id file-type))))
-    (when (and denote--used-ids (not (string-empty-p id)))
-      (puthash id t denote--used-ids))
+          (denote-prepend-front-matter new-name title keywords signature date 
identifier file-type))))
+    (when (and denote--used-ids (not (string-empty-p identifier)))
+      (puthash identifier t denote--used-ids))
     (denote--handle-save-and-kill-buffer 'rename new-name initial-state)
     (setq denote-current-data
           (list
@@ -3973,7 +4098,7 @@ Respect `denote-rename-confirmations', 
`denote-save-buffers' and
            (cons 'signature signature)
            (cons 'directory directory)
            (cons 'date date)
-           (cons 'id id)
+           (cons 'id identifier)
            (cons 'file-type file-type)
            (cons 'template "")))
     (run-hooks 'denote-after-rename-file-hook)
@@ -3986,8 +4111,8 @@ It is meant to be combined with `denote--rename-file' to 
create
 renaming commands."
   (let* ((file-in-prompt (propertize (file-relative-name file) 'face 
'denote-faces-prompt-current-name))
          (file-type (denote-filetype-heuristics file))
-         (id (or (denote-retrieve-filename-identifier file) ""))
-         (date (or (denote-valid-date-p id) (denote--generate-date-for-rename 
file)))
+         (date (denote-retrieve-front-matter-date-value file file-type))
+         (identifier (or (denote-retrieve-filename-identifier file) ""))
          (title (or (denote-retrieve-title-or-filename file file-type) ""))
          (keywords (denote-extract-keywords-from-path file))
          (signature (or (denote-retrieve-filename-signature file) "")))
@@ -4005,14 +4130,18 @@ renaming commands."
          (setq signature (denote-signature-prompt
                           signature
                           (format "Rename `%s' with SIGNATURE (empty to 
remove)" file-in-prompt))))
+        ('identifier
+         (setq identifier (denote-identifier-prompt
+                           identifier
+                           (format "Rename `%s' with IDENTIFIER (empty to 
remove)" file-in-prompt))))
         ('date
          (setq date (denote-valid-date-p (denote-date-prompt
                                           date
                                           (format "Rename `%s' with DATE" 
file-in-prompt)))))))
-    (list title keywords signature date)))
+    (list title keywords signature date identifier)))
 
 ;;;###autoload
-(defun denote-rename-file (file title keywords signature date)
+(defun denote-rename-file (file title keywords signature date identifier)
   "Rename file and update existing front matter if appropriate.
 
 Always rename the file where it is located in the file system:
@@ -4030,24 +4159,13 @@ KEYWORDS.  The SIGNATURE is another one.  When called 
from Lisp,
 TITLE and SIGNATURE are strings, while KEYWORDS is a list of
 strings.
 
-If there is no identifier, create an identifier based on the
-following conditions:
-
-1. If the `denote-prompts' includes an entry for date prompts,
-   then prompt for DATE and take its input to produce a new
-   identifier.  For use in Lisp, DATE must conform with
-   `denote-valid-date-p'.
-
-2. If DATE is nil (e.g. when `denote-prompts' does not include a
-   date entry), use the file attributes to determine the last
-   modified date of FILE and format it as an identifier.
-
-3. As a fallback, derive an identifier from the current date and
-   time.
+The IDENTIFIER is a string that has the format of variable
+`denote-date-identifier-format'.
 
-4. At any rate, if the resulting identifier is not unique among
-   the files in the variable `denote-directory', increment it
-   such that it becomes unique.
+If there is no identifier, create a new identifier using
+`denote--identifier-generation-function'. By default, it creates a new
+identifier using the date parameter, the date of last modification or
+the `current-time'.
 
 In interactive use, and assuming `denote-prompts' includes a
 title entry, make the TITLE prompt have prefilled text in the
@@ -4129,8 +4247,10 @@ file-naming scheme.
 For a version of this command that works with multiple files
 one-by-one, use `denote-dired-rename-files'."
   (interactive
-   (let* ((file (denote--rename-dired-file-or-current-file-or-prompt)))
-     (append (list file) 
(denote--rename-get-file-info-from-prompts-or-existing file))))
+   (pcase-let* ((file (denote--rename-dired-file-or-current-file-or-prompt))
+                (`(,title ,keywords ,signature ,date ,identifier)
+                 (denote--rename-get-file-info-from-prompts-or-existing file)))
+     (list file title keywords signature date identifier)))
   (let* ((file-type (denote-filetype-heuristics file))
          (title (if (eq title 'keep-current)
                     (or (denote-retrieve-title-or-filename file file-type) "")
@@ -4144,9 +4264,12 @@ one-by-one, use `denote-dired-rename-files'."
          (date (if (eq date 'keep-current)
                    (denote-retrieve-filename-identifier file)
                  date))
+         (identifier (if (eq identifier 'keep-current)
+                         (or (denote-retrieve-filename-identifier file) "")
+                       identifier))
          ;; Make the data valid
          (date (denote-valid-date-p date))
-         (new-name (denote--rename-file file title keywords signature date)))
+         (new-name (denote--rename-file file title keywords signature date 
identifier)))
     (denote-update-dired-buffers)
     new-name))
 
@@ -4192,6 +4315,22 @@ Modify a date in one go."
   (let ((denote-prompts '(date)))
     (call-interactively #'denote-rename-file)))
 
+(defun denote-rename-file-identifier ()
+  "Convenience command to change the identifier of a file.
+Like `denote-rename-file', but prompts only for the identifier.
+
+Modify an identifier in one go.  Do this by prepopulating the
+minibuffer prompt with the existing identifier.  The user can then modify
+it accordingly.  An empty input means to remove the identifier
+altogether.
+
+Please check the documentation of `denote-rename-file' with regard to
+how a completion User Interface may accept an empty input."
+  (declare (interactive-only t))
+  (interactive)
+  (let ((denote-prompts '(identifier)))
+    (call-interactively #'denote-rename-file)))
+
 (define-obsolete-function-alias 'denote-keywords-add 
'denote-rename-file-keywords "3.0.0")
 (define-obsolete-function-alias 'denote-rename-add-keywords 
'denote-rename-file-keywords "3.0.0")
 (define-obsolete-function-alias 'denote-keywords-remove 
'denote-rename-file-keywords "3.0.0")
@@ -4230,9 +4369,9 @@ setting `denote-rename-confirmations' to a nil value)."
     (if-let* ((marks (dired-get-marked-files)))
         (progn
           (dolist (file marks)
-            (pcase-let ((`(,title ,keywords ,signature ,date)
+            (pcase-let ((`(,title ,keywords ,signature ,date ,identifier)
                          
(denote--rename-get-file-info-from-prompts-or-existing file)))
-              (denote--rename-file file title keywords signature date)))
+              (denote--rename-file file title keywords signature date 
identifier)))
           (denote-update-dired-buffers))
       (user-error "No marked files; aborting"))))
 
@@ -4270,10 +4409,10 @@ This function is an internal implementation function."
             (user-input-keywords (denote-keywords-prompt keywords-prompt))
             (denote--used-ids (denote--get-all-used-ids)))
         (dolist (file marks)
-          (pcase-let* ((`(,title ,keywords ,signature ,date)
+          (pcase-let* ((`(,title ,keywords ,signature ,date ,identifier)
                         (denote--rename-get-file-info-from-prompts-or-existing 
file))
                        (new-keywords (denote-keywords-sort 
(denote-keywords--combine combination-type user-input-keywords keywords))))
-            (denote--rename-file file title new-keywords signature date)))
+            (denote--rename-file file title new-keywords signature date 
identifier)))
         (denote-update-dired-buffers))
     (user-error "No marked files; aborting")))
 
@@ -4380,14 +4519,14 @@ Construct the file name in accordance with the user 
option
            (signature (if (memq 'signature components-in-template)
                           (or (denote-retrieve-front-matter-signature-value 
file file-type) "")
                         (or (denote-retrieve-filename-signature file) "")))
-           ;; We need to use the identifier because the date line may
-           ;; not contain all the information.  For example,
-           ;; "2024-01-01" does not have the time of the note.
-           (date (if (memq 'identifier components-in-template)
-                     (when-let* ((id-value 
(denote-retrieve-front-matter-identifier-value file file-type)))
-                       (denote-valid-date-p id-value))
-                   (denote-valid-date-p (or 
(denote-retrieve-filename-identifier file) "")))))
-      (denote--rename-file file title keywords signature date)
+           (identifier (if (memq 'identifier components-in-template)
+                           (or (denote-retrieve-front-matter-identifier-value 
file file-type) "")
+                         (or (denote-retrieve-filename-identifier file) "")))
+           (date (when (memq 'date components-in-template)
+                   (when-let* ((date-value 
(denote-retrieve-front-matter-date-value file file-type)))
+                     (denote-valid-date-p date-value))))
+           (denote-accept-nil-date t))
+      (denote--rename-file file title keywords signature date identifier)
       (denote-update-dired-buffers))))
 
 ;;;###autoload
@@ -4448,7 +4587,7 @@ Construct the file name in accordance with the user option
          (dir (file-name-directory file))
          (old-file-type (denote-filetype-heuristics file))
          (id (or (denote-retrieve-filename-identifier file) ""))
-         (date (if (string-empty-p id) nil (date-to-time id)))
+         (date (denote-retrieve-front-matter-date-value file old-file-type))
          (title (or (denote-retrieve-title-or-filename file old-file-type) ""))
          (keywords (denote-retrieve-front-matter-keywords-value file 
old-file-type))
          (signature (or (denote-retrieve-filename-signature file) ""))
@@ -4966,14 +5105,14 @@ active, use it as the description."
                             ;; TODO 2025-04-03: Maybe we can have something 
like `denote-date-format' here,
                             ;; but I think we are okay with a hardcoded value.
                             (cons ?I (or (when-let* ((id 
(denote-retrieve-filename-identifier file))
-                                                     (_ (denote-valid-date-p 
id)))
+                                                     (_ 
(denote-date-identifier-p id)))
                                            (format-time-string "%A, %e %B %Y" 
(date-to-time (denote-id-to-date id))))
                                          ""))
                             (cons ?D (cond
                                       
((denote-retrieve-front-matter-title-value file type))
                                       ((denote-retrieve-filename-title file))
                                       ((when-let* ((id 
(denote-retrieve-filename-identifier file)))
-                                         (if (denote-valid-date-p id)
+                                         (if (denote-date-identifier-p id)
                                              (format-time-string "%A, %e %B 
%Y" (date-to-time (denote-id-to-date id)))
                                            id)))
                                       (t  "")))
@@ -5935,9 +6074,10 @@ To be assigned to `markdown-follow-link-functions'."
 
 (defun denote-get-link-face (query)
   "Return appropriate face for QUERY."
-  (if (denote-identifier-p (string-trim-right query ":[^/]+.*"))
-      'denote-faces-link
-    'denote-faces-query-link))
+  (if (or (string-prefix-p "query-contents:" query)
+          (string-prefix-p "query-filenames:" query))
+      'denote-faces-query-link
+    'denote-faces-link))
 
 ;; Implementation based on the function `org-activate-links'.
 (defun denote-fontify-links (limit)
@@ -6542,16 +6682,16 @@ text).
 
 Consult the manual for template samples."
   (pcase-let* ((denote-prompts (remove 'file-type denote-prompts)) ; Do not 
prompt for file-type. We use org.
-               (`(,title ,keywords _ ,directory ,date ,template ,signature)
+               (`(,title ,keywords _ ,directory ,date ,identifier ,template 
,signature)
                 (denote--creation-get-note-data-from-prompts))
-               (`(,title ,keywords _ ,directory ,date ,id ,template ,signature)
-                (denote--creation-prepare-note-data title keywords 'org 
directory date template signature))
-               (front-matter (denote--format-front-matter title date keywords 
id signature 'org))
+               (`(,title ,keywords _ ,directory ,date ,identifier ,template 
,signature)
+                (denote--creation-prepare-note-data title keywords 'org 
directory date identifier template signature))
+               (front-matter (denote--format-front-matter title date keywords 
identifier signature 'org))
                (template-string (cond ((stringp template) template)
                                       ((functionp template) (funcall template))
                                       (t (user-error "Invalid template")))))
     (setq denote-last-path
-          (denote-format-file-name directory id keywords title ".org" 
signature))
+          (denote-format-file-name directory identifier keywords title ".org" 
signature))
     (when (file-regular-p denote-last-path)
       (user-error "A file named `%s' already exists" denote-last-path))
     (denote--keywords-add-to-history keywords)
@@ -6690,7 +6830,7 @@ buffer will be used, if available."
                           ;; TODO 2025-04-03: Maybe we can have something like 
`denote-date-format' here,
                           ;; but I think we are okay with a hardcoded value.
                           (cons ?I (or (when-let* ((id 
(denote-retrieve-filename-identifier file))
-                                                   (_ (denote-valid-date-p 
id)))
+                                                   (_ 
(denote-date-identifier-p id)))
                                          (format-time-string "%A, %e %B %Y" 
(date-to-time (denote-id-to-date id))))
                                        ""))
                           (cons ?d (or (denote-retrieve-filename-identifier 
file) ""))
@@ -6698,7 +6838,7 @@ buffer will be used, if available."
                                     ((denote-retrieve-front-matter-title-value 
file type))
                                     ((denote-retrieve-filename-title file))
                                     ((when-let* ((id 
(denote-retrieve-filename-identifier file)))
-                                       (if (denote-valid-date-p id)
+                                       (if (denote-date-identifier-p id)
                                            (format-time-string "%A, %e %B %Y" 
(date-to-time (denote-id-to-date id)))
                                          id)))
                                     (t  "")))
diff --git a/tests/denote-test.el b/tests/denote-test.el
index bad777ad69..ee87fbe02c 100644
--- a/tests/denote-test.el
+++ b/tests/denote-test.el
@@ -415,12 +415,6 @@ does not involve the time zone."
                (eq (denote-filetype-heuristics 
"20231010T105034--some-test-file__denote_testing.md.gpg") 'markdown-yaml)
                (eq (denote-filetype-heuristics 
"20231010T105034--some-test-file__denote_testing.md.age") 'markdown-yaml))))
 
-(ert-deftest dt-denote-get-identifier ()
-  "Test that `denote-get-identifier' returns an identifier."
-  (should (and (equal (denote-get-identifier nil) "")
-               (equal (denote-get-identifier 1705644188) "20240119T080308")
-               (equal (denote-get-identifier '(26026 4251)) 
"20240119T080307"))))
-
 (ert-deftest dt-denote-retrieve-filename-identifier ()
   "Test that `denote-retrieve-filename-identifier' returns only the 
identifier."
   (should (and (null
@@ -515,10 +509,10 @@ does not involve the time zone."
   (should (and (denote-identifier-p "20240901T090910")
                (null (denote-identifier-p 
"20240901T090910-not-identifier-format")))))
 
-(ert-deftest dt-denote--id-to-date ()
-  "Test that `denote--id-to-date' returns the date from an identifier."
-  (should (equal (denote--id-to-date "20240901T090910") "2024-09-01"))
-  (should-error (denote--id-to-date "20240901T090910-not-identifier-format")))
+(ert-deftest dt-denote-id-to-date ()
+  "Test that `denote-id-to-date' returns the date from an identifier."
+  (should (equal (denote-id-to-date "20240901T090910") "2024-09-01"))
+  (should-error (denote-id-to-date "20240901T090910-not-identifier-format")))
 
 (ert-deftest dt-denote--date-convert ()
   "Test that `denote--date-convert' works with dates."

Reply via email to