mirror of
https://github.com/protesilaos/denote.git
synced 2026-09-10 07:16:20 -04:00
denote-{identifier,signature,title,keywords}-regexp used a lazy
zero-or-more capture ([^.]*?), so they matched the bare separator
followed by an extension, e.g. __.js, --.org, ==.org, @@.org. Combined
with denote-file-has-denoted-filename-p, this meant such files were
treated as valid denote notes. lodash's node_modules ships __.js files,
which made a denote-directory containing a lodash checkout enumerate
junk candidates in the file prompt (and previously crash, see preceding
commit).
Switch to a non-empty lazy capture ([^.]+?) so the field must contain at
least one character.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7350 lines
317 KiB
EmacsLisp
7350 lines
317 KiB
EmacsLisp
;;; denote.el --- Simple notes with an efficient file-naming scheme -*- lexical-binding: t -*-
|
||
|
||
;; Copyright (C) 2022-2026 Free Software Foundation, Inc.
|
||
|
||
;; Author: Protesilaos <info@protesilaos.com>
|
||
;; Maintainer: Protesilaos <info@protesilaos.com>
|
||
;; URL: https://github.com/protesilaos/denote
|
||
;; Version: 4.2.0
|
||
;; Package-Requires: ((emacs "28.1"))
|
||
|
||
;; 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:
|
||
|
||
;; Denote is a simple note-taking tool for Emacs. It is based on the idea
|
||
;; that notes should follow a predictable and descriptive file-naming
|
||
;; scheme. The file name must offer a clear indication of what the note is
|
||
;; about, without reference to any other metadata. Denote basically
|
||
;; streamlines the creation of such files while providing facilities to
|
||
;; link between them.
|
||
;;
|
||
;; Denote's file-naming scheme is not limited to "notes". It can be used
|
||
;; for all types of file, including those that are not editable in Emacs,
|
||
;; such as videos. Naming files in a consistent way makes their
|
||
;; filtering and retrieval considerably easier. Denote provides relevant
|
||
;; facilities to rename files, regardless of file type.
|
||
|
||
;;; Code:
|
||
|
||
(require 'seq)
|
||
(require 'xref)
|
||
(require 'dired)
|
||
(eval-when-compile (require 'subr-x))
|
||
|
||
(defgroup denote ()
|
||
"Simple notes with an efficient file-naming scheme."
|
||
:group 'files
|
||
:link '(info-link "(denote) Top")
|
||
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
|
||
|
||
;;;; User options
|
||
|
||
;; About the autoload: (info "(elisp) File Local Variables")
|
||
|
||
;;;###autoload (put 'denote-directory 'safe-local-variable (lambda (val) (or (stringp val) (listp val) (eq val 'local) (eq val 'default-directory))))
|
||
(defcustom denote-directory (expand-file-name "~/Documents/notes/")
|
||
"Directory, as a string, for storing personal notes.
|
||
This is the destination `denote' and all other file-creating Denote
|
||
commands use.
|
||
|
||
The value can also be a list of directories as strings. In that case,
|
||
`denote' and related commands will pick the first one among them, unless
|
||
the user option `denote-prompts' is configured to prompt for a
|
||
directory, among other possible prompts. Files within those directories
|
||
can link to each other, as they are considered part of one expansive
|
||
Denote directory.
|
||
|
||
Whether the value is a string or a list of strings, all relevant Denote
|
||
commands work with the subdirectories of those directories as well. In
|
||
other words, a list value is not needed to enumerate all the
|
||
subdirectories of a common parent directory: simply specify the parent.
|
||
|
||
To create a \"silo\", i.e. a self-contained Denote directory whose
|
||
files do no link to any file outside of it, set the value of this user
|
||
option in a .dir-locals file. Read Info node `(denote) Maintain
|
||
separate directory silos for notes'.
|
||
|
||
If the target directory does not exist, `denote' and related commands
|
||
will create it.
|
||
|
||
The value of this variable is read by commands other than `denote', such
|
||
as `denote-link', `denote-backlinks', `denote-dired', and `denote-grep',
|
||
among others. File-renaming commands such as `denote-rename-file' and
|
||
`denote-dired-rename-marked-files' can still be used on any file
|
||
anywhere on the file system.
|
||
|
||
To use the value of this variable from Lisp, call the function
|
||
`denote-directories'."
|
||
:group 'denote
|
||
:safe (lambda (val) (or (stringp val) (listp val) (eq val 'local) (eq val 'default-directory)))
|
||
:package-version '(denote . "4.1.0")
|
||
:link '(info-link "(denote) Maintain separate directories for notes")
|
||
:type '(choice (directory :tag "Single directory")
|
||
(repeat :tag "List of directories" directory)))
|
||
|
||
(define-obsolete-variable-alias 'denote-save-buffer-after-creation 'denote-save-buffers "3.0.0")
|
||
|
||
(defcustom denote-save-buffers nil
|
||
"Control whether to save buffers automatically.
|
||
This applies to commands that create new notes, such as `denote', or
|
||
those that rename existing files, such as `denote-rename-file'.
|
||
|
||
By default (a nil value), Denote does not save such buffers, giving
|
||
users the chance to review the text before writing it to the file.
|
||
|
||
When the value is non-nil, Denote will automatically save the buffers it
|
||
modifies.
|
||
|
||
Also see `denote-kill-buffers'."
|
||
:group 'denote
|
||
:package-version '(denote . "3.0.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-kill-buffers nil
|
||
"Control whether creation or renaming commands kill their buffer.
|
||
|
||
The default behaviour of creation or renaming commands such as
|
||
`denote' or `denote-rename-file' is to not kill the buffer they
|
||
create or modify at the end of their operation.
|
||
|
||
If this user option is nil (the default), buffers affected by a
|
||
creation or renaming command are not automatically killed.
|
||
|
||
If set to `on-creation', new notes are automatically killed.
|
||
|
||
If set to `on-rename', renamed notes are automatically killed.
|
||
|
||
If set to t, new and renamed notes are killed.
|
||
|
||
If a buffer is killed, it is also saved, as if `denote-save-buffers'
|
||
were t. See its documentation.
|
||
|
||
In all cases, if the buffer already existed before the Denote operation
|
||
it is NOT automatically killed."
|
||
:group 'denote
|
||
:package-version '(denote . "3.1.0")
|
||
:type '(choice
|
||
(const :tag "Do not kill buffers" nil)
|
||
(const :tag "Kill after creation" on-creation)
|
||
(const :tag "Kill after rename" on-rename)
|
||
(const :tag "Kill after creation and rename" t)))
|
||
|
||
;;;###autoload (put 'denote-known-keywords 'safe-local-variable #'listp)
|
||
(defcustom denote-known-keywords
|
||
'("emacs" "philosophy" "politics" "economics")
|
||
"List of strings with predefined keywords.
|
||
This is used by the `denote-keywords-prompt' to get keywords when
|
||
creating or renaming a file, such as via the commands `denote' and
|
||
`denote-rename-file'.
|
||
|
||
The `denote-keywords-prompt' does not enforce the keywords defined in
|
||
`denote-known-keywords', as users can input arbitrary text. Those newly
|
||
introduced keywords are then available for completion, if
|
||
`denote-infer-keywords' is set to a non-nil value (its default). If
|
||
`denote-infer-keywords' is nil, then the `denote-keywords-prompt' only
|
||
accepts input that is among the `denote-known-keywords'.
|
||
|
||
Also see: `denote-sort-keywords', `denote-file-name-slug-functions'."
|
||
:group 'denote
|
||
:safe #'listp
|
||
:package-version '(denote . "0.1.0")
|
||
:type '(repeat string))
|
||
|
||
;;;###autoload (put 'denote-infer-keywords 'safe-local-variable (lambda (val) (or val (null val))))
|
||
(defcustom denote-infer-keywords t
|
||
"Whether to infer keywords from existing notes' file names.
|
||
|
||
When non-nil (the default), search the file names of existing notes in
|
||
the variable `denote-directory' for their keyword field and extract the
|
||
entries as \"inferred keywords\". These are combined with
|
||
`denote-known-keywords' and are presented as completion candidates while
|
||
using `denote' and related commands interactively.
|
||
|
||
If nil, refrain from inferring keywords. In this case, make the
|
||
aforementioned completion prompt show just the `denote-known-keywords'
|
||
and enforce them as the only acceptable input. Use this if you want to
|
||
work with a controlled vocabulary.
|
||
|
||
The user option `denote-keywords-to-not-infer-regexp' can be used to
|
||
exclude keywords that match a regular expression.
|
||
|
||
Inferred keywords are specific to the value of the variable
|
||
`denote-directory'. In a silo, as explained in that variable's doc
|
||
string, the inferred keywords are specific to the silo."
|
||
:group 'denote
|
||
:safe (lambda (val) (or val (null val)))
|
||
:package-version '(denote . "4.2.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-prompts '(title keywords)
|
||
"Specify the prompts followed by relevant Denote commands.
|
||
|
||
Commands that prompt for user input to construct a Denote file name
|
||
include, but are not limited to: `denote', `denote-signature',
|
||
`denote-type', `denote-date', `denote-subdirectory',
|
||
`denote-rename-file', `denote-dired-rename-files'.
|
||
|
||
The value of this user option is a list of symbols, which includes any
|
||
of the following:
|
||
|
||
- `title': Prompt for the title of the new note.
|
||
|
||
- `keywords': Prompts with completion for the keywords of the new note.
|
||
Available candidates are those specified in the user option
|
||
`denote-known-keywords'. If the user option `denote-infer-keywords'
|
||
is non-nil, keywords in existing note file names are included in the
|
||
list of candidates. The `keywords' prompt uses `completing-read-multiple',
|
||
meaning that it can accept multiple keywords separated by a comma (or
|
||
whatever the value of `crm-separator' is).
|
||
|
||
- `file-type': Prompts with completion for the file type of the new
|
||
note. Available candidates are those specified in the user option
|
||
`denote-file-type'. Without this prompt, `denote' uses the value of
|
||
the variable `denote-file-type'.
|
||
|
||
- `subdirectory': Prompts with completion for a subdirectory in which to
|
||
create the note. Available candidates are the value of the user
|
||
option `denote-directory' and all of its subdirectories. Any
|
||
subdirectory must already exist: Denote will not create it.
|
||
|
||
- `date': Prompts for the date of the new note. It will expect an input
|
||
like 2022-06-16 or a date plus time: 2022-06-16 14:30. Without the
|
||
`date' prompt, the `denote' command uses the `current-time'. (To
|
||
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.
|
||
|
||
- `signature': Prompts for an arbitrary string that can be used for any
|
||
kind of workflow, such as a special tag to label the part1 and part2
|
||
of a large file that is split in half, or to add special contexts like
|
||
home and work, or even priorities like a, b, c. One other use-case is
|
||
to implement a sequencing scheme that makes notes have hierarchical
|
||
relationships. This is handled by our optional extension
|
||
denote-sequence.el, which is its own package (read the manual).
|
||
|
||
The prompts occur in the given order.
|
||
|
||
If the value of this user option is nil, no prompts are used. The
|
||
resulting file name will consist of an identifier (i.e. the date and
|
||
time) and a supported file type extension (per the variable
|
||
`denote-file-type').
|
||
|
||
Recall that Denote's standard file-naming scheme is defined as
|
||
follows (read the manual for the technicalities):
|
||
|
||
ID--TITLE__KEYWORDS.EXT
|
||
|
||
Depending on the inclusion of the `title', `keywords', and
|
||
`signature' prompts, file names will be any of those
|
||
permutations:
|
||
|
||
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).
|
||
|
||
Finally, this user option only affects the interactive use of the
|
||
`denote' or other relevant commands (advanced users can call it from
|
||
Lisp). In Lisp usage, the behaviour is always what the caller
|
||
specifies, based on the supplied arguments.
|
||
|
||
Also see `denote-history-completion-in-prompts'.
|
||
|
||
To change the order of the file name components, refer to
|
||
`denote-file-name-components-order'."
|
||
:group 'denote
|
||
:package-version '(denote . "2.3.0")
|
||
:link '(info-link "(denote) The denote-prompts option")
|
||
:type '(radio (const :tag "Use no prompts" nil)
|
||
(set :tag "Available prompts" :greedy t
|
||
(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)
|
||
(const :tag "Signature" signature))))
|
||
|
||
(defcustom denote-file-name-components-order '(identifier signature title keywords)
|
||
"Specify the order of the file name components.
|
||
|
||
The value is a list of the following symbols:
|
||
|
||
- `identifier': This is the combination of the date and time. When it
|
||
is the first on the list, it looks like \"20240519T073456\" and does
|
||
not have a component separator of its own due its unambiguous format.
|
||
When it is placed anywhere else in the file name, it is prefixed with
|
||
\"@@\", so it looks like \"@@20240519T073456\".
|
||
|
||
- `signature': This is an arbitrary string that can be used to qualify
|
||
the file in some way, according to the user's methodology (e.g. to add
|
||
a sequence to notes). The string is always prefixed with the \"==\"
|
||
to remain unambiguous.
|
||
|
||
- `title': This is an arbitrary string which describes the file. It is
|
||
always prefixed with \"--\" to be unambiguous.
|
||
|
||
- `keywords': This is a series of one or more words that succinctly
|
||
group the file. Multiple keywords are separated by an underscore
|
||
prefixed to each of them. The file name component is always prefixed
|
||
with \"__\".
|
||
|
||
All four symbols must appear exactly once. Duplicates are ignored. Any
|
||
missing symbol is added automatically.
|
||
|
||
Some examples:
|
||
|
||
(setq denote-file-name-components-order
|
||
\\='(identifier signature title keywords))
|
||
=> 20240519T07345==hello--this-is-the-title__denote_testing.org
|
||
|
||
(setq denote-file-name-components-order
|
||
\\='(signature identifier title keywords))
|
||
=> ==hello@@20240519T07345--this-is-the-title__denote_testing.org
|
||
|
||
(setq denote-file-name-components-order
|
||
\\='(title signature identifier keywords))
|
||
=> --this-is-the-title==hello@@20240519T07345__denote_testing.org
|
||
|
||
(setq denote-file-name-components-order
|
||
\\='(keywords title signature identifier))
|
||
=> __denote_testing--this-is-the-title==hello@@20240519T07345.org
|
||
|
||
Also see the user option `denote-prompts', which affects which
|
||
components are actually used in the order specified herein.
|
||
|
||
Before deciding on this, please consider the longer-term implications
|
||
of file names with varying patterns. Consistency makes things
|
||
predictable and thus easier to find. So pick one order and never touch
|
||
it again. When in doubt, leave the default file-naming scheme as-is.
|
||
|
||
This user option should only be used to build a file name. Custom code
|
||
should not have behaviors that depend on its value. The reason is that
|
||
its value can change over time and Denote should be able to handle past
|
||
and current notes."
|
||
:group 'denote
|
||
:package-version '(denote . "3.0.0")
|
||
;; FIXME 2024-05-19: This technically works to display the user
|
||
;; option in the Custom buffer and to show its current value, though
|
||
;; it does not allow the user to modify it graphically: they have to
|
||
;; switch to the Lisp expression. Find a way to present an
|
||
;; interface that lets the user reorder those elements.
|
||
;;
|
||
;; Still, making this a defcustom helps with discoverability, as
|
||
;; well as with the use of `setopt' and related.
|
||
:type '(list
|
||
(const :tag "Identifier component (date and time)" identifier)
|
||
(const :tag "File signature (text to qualify a file)" signature)
|
||
(const :tag "The title of the file" title)
|
||
(const :tag "Keywords of the file" keywords)))
|
||
|
||
(defcustom denote-front-matter-components-present-even-if-empty-value '(title keywords date identifier)
|
||
"The components that are always present in front matter even when empty.
|
||
|
||
Components are `title', `keywords', `signature', `date', `identifier'.
|
||
|
||
Note that even though a component may be listed in this variable, it
|
||
will not be present in the front matter if the corresponding line is not
|
||
in the front matter template."
|
||
:group 'denote
|
||
:package-version '(denote . "4.0.0")
|
||
:type '(list
|
||
(const :tag "Title" title)
|
||
(const :tag "Keywords" keywords)
|
||
(const :tag "Signature" signature)
|
||
(const :tag "Date" date)
|
||
(const :tag "Identifier" identifier)))
|
||
|
||
(defcustom denote-identifier-delimiter-always-present-in-file-name nil
|
||
"Specify if file names always contain the identifier delimiter."
|
||
:group 'denote
|
||
:package-version '(denote . "4.1.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-sort-keywords t
|
||
"Whether to sort keywords in new files.
|
||
|
||
When non-nil, the keywords of `denote' are sorted with
|
||
`string-collate-lessp' regardless of the order they were inserted at the
|
||
minibuffer prompt.
|
||
|
||
If nil, show the keywords in their given order."
|
||
:group 'denote
|
||
:package-version '(denote . "0.1.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-file-type 'org
|
||
"The file type extension for new notes.
|
||
|
||
When the value is `org' (the default), the file type is that of Org
|
||
mode.
|
||
|
||
When the value is the symbol `markdown-yaml', the file type is that of
|
||
Markdown mode and the front matter uses YAML notation. Similarly,
|
||
`markdown-toml' is Markdown but has TOML syntax in the front matter.
|
||
|
||
When the value is `text', the file type is that of Text mode.
|
||
|
||
Any other value makes Denote use the first element of the
|
||
`denote-file-types', which is the registry with all the supported file
|
||
types (Org is the first there out-of-the-box, but users may modify it).
|
||
|
||
NOTE: Expert users can change the supported file-types by editing the
|
||
value of `denote-file-types'. That variable, which is not a user
|
||
option, controls the behaviour of all file-type-aware
|
||
functions (creating notes, renaming them, inserting front matter,
|
||
formatting a link, etc.). Consult its documentation for the
|
||
technicalities."
|
||
:type '(choice
|
||
(const :tag "Unspecified (defaults to Org)" nil)
|
||
(const :tag "Org mode (default)" org)
|
||
(const :tag "Markdown (YAML front matter)" markdown-yaml)
|
||
(const :tag "Markdown (TOML front matter)" markdown-toml)
|
||
(const :tag "Plain text" text))
|
||
:package-version '(denote . "4.2.0")
|
||
:group 'denote)
|
||
|
||
(defcustom denote-date-format nil
|
||
"Date format in the front matter (file header) of new notes.
|
||
|
||
When nil (the default value), use a file-type-specific
|
||
format (also check the user option `denote-file-type'):
|
||
|
||
- For Org, an inactive timestamp is used, such as [2022-06-30 Wed
|
||
15:31].
|
||
|
||
- For Markdown, the RFC3339 standard is applied:
|
||
2022-06-30T15:48:00+03:00.
|
||
|
||
- For plain text, the format is that of ISO 8601: 2022-06-30.
|
||
|
||
If the value is a string, ignore the above and use it instead.
|
||
The string must include format specifiers for the date. These
|
||
are described in the docstring of `format-time-string'."
|
||
:type '(choice
|
||
(const :tag "Use appropiate format for each file type" nil)
|
||
(string :tag "Custom format for `format-time-string'"))
|
||
:package-version '(denote . "0.2.0")
|
||
:group 'denote)
|
||
|
||
(defcustom denote-date-prompt-use-org-read-date nil
|
||
"Whether to use `org-read-date' in date prompts.
|
||
|
||
If non-nil, use `org-read-date'. If nil, input the date as a
|
||
string, as described in `denote'.
|
||
|
||
This option is relevant when `denote-prompts' includes a `date'
|
||
and/or when the user invokes the command `denote-date'."
|
||
:group 'denote
|
||
:package-version '(denote . "0.6.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-org-store-link-to-heading nil
|
||
"Determine whether `org-store-link' links to the current Org heading.
|
||
|
||
[ Remember that what `org-store-link' does is merely collect a link. To
|
||
actually insert it, use the command `org-insert-link'. Note that
|
||
`org-capture' uses `org-store-link' internally when it needs to store
|
||
a link. ]
|
||
|
||
When the value is nil, the Denote handler for `org-store-link' produces
|
||
links only to the current file (by using the file's identifier). For
|
||
example:
|
||
|
||
[[denote:20240118T060608][Some test]]
|
||
|
||
If the value is `context', the link consists of the file's identifier
|
||
and the text of the current heading, like this:
|
||
|
||
[[denote:20240118T060608::*Heading text][Some test::Heading text]].
|
||
|
||
However, if there already exists a CUSTOM_ID property for the current
|
||
heading, this is always given priority and is used instead of the
|
||
context.
|
||
|
||
If the value is `id' or, for backward-compatibility, any other non-nil
|
||
value, then Denote will use the standard Org mechanism of the CUSTOM_ID
|
||
property to create a unique link to the heading. If the heading does
|
||
not have a CUSTOM_ID, it creates it and includes it in its PROPERTIES
|
||
drawer. If a CUSTOM_ID exists, it takes it as-is. The result is like
|
||
this:
|
||
|
||
[[denote:20240118T060608::#h:eed0fb8e-4cc7-478f][Some test::Heading text]]
|
||
|
||
The value of the CUSTOM_ID is determined by the Org user option
|
||
`org-id-method'. The sample shown above uses the default UUID
|
||
infrastructure (though I deleted a few characters to not get
|
||
complaints from the byte compiler about long lines in the doc
|
||
string...).
|
||
|
||
Note that this option does not affect how Org behaves with regard to
|
||
`org-id-link-to-org-use-id'. If that user option is set to create ID
|
||
properties, then those will be created by Org even if the Denote link
|
||
handler will take care to not use/store the ID value. Concretely, users
|
||
who never want ID properties under their headings should keep
|
||
`org-id-link-to-org-use-id' in its nil value.
|
||
|
||
Context links are easier to break than those with a CUSTOM_ID in cases
|
||
where either the heading text changes or there is another heading that
|
||
matches that text. The potential advantage of context links is that
|
||
they do not require a PROPERTIES drawer.
|
||
|
||
When visiting a link to a heading, Org opens the Denote file and then
|
||
navigates to that heading.
|
||
|
||
[ This feature only works in Org mode files, as other file types
|
||
do not have a linking mechanism that handles unique identifiers
|
||
for headings or other patterns to jump to. If `org-store-link'
|
||
is invoked in one such file, it captures only the Denote
|
||
identifier of the file, even if this user option is set to a
|
||
non-nil value. ]"
|
||
:group 'denote
|
||
:package-version '(denote . "4.0.0")
|
||
:type '(choice (const :tag "No link to heading (default)" nil)
|
||
(const :tag "Link to the context" context)
|
||
(const :tag "Link wtih CUSTOM_ID, creating it if needed" id)))
|
||
|
||
(defcustom denote-templates nil
|
||
"Alist of content templates for new notes.
|
||
A template is arbitrary text that Denote will add to a newly
|
||
created note right below the front matter.
|
||
|
||
Templates are expressed as a (KEY . VALUE) association.
|
||
|
||
- The KEY is the name which identifies the template. It is an
|
||
arbitrary symbol, such as `report', `memo', `statement'.
|
||
|
||
- The VALUE is either a string or the symbol of a function.
|
||
|
||
- If it is a string, it is ordinary text that Denote will insert
|
||
as-is. It can contain newline characters to add spacing. The
|
||
manual of Denote contains examples on how to use the `concat'
|
||
function, beside writing a generic string.
|
||
|
||
- If it is a function, it is called without arguments and is expected
|
||
to return a string. Denote will call the function and insert the
|
||
result in the buffer.
|
||
|
||
The user can choose a template either by invoking the command
|
||
`denote-template' or by changing the user option `denote-prompts'
|
||
to always prompt for a template when calling the `denote'
|
||
command."
|
||
:type '(alist :key-type symbol :value-type (choice string function))
|
||
:package-version '(denote . "3.1.0")
|
||
:link '(info-link "(denote) The denote-templates option")
|
||
:group 'denote)
|
||
|
||
(defcustom denote-rename-confirmations '(rewrite-front-matter modify-file-name)
|
||
"Make renaming commands prompt for confirmations.
|
||
|
||
This affects the behaviour of renaming commands. The value is either
|
||
nil, in which case no confirmation is ever requested, or a list of
|
||
symbols among the following:
|
||
|
||
- `modify-file-name' means that renaming commands will ask for
|
||
confirmation before modifying the file name.
|
||
|
||
- `rewrite-front-matter' means that renaming commands will ask for
|
||
confirmation before rewritting the front matter.
|
||
|
||
- `add-front-matter' means that renaming commands will ask for
|
||
confirmation before adding new front matter to the file.
|
||
|
||
The default behaviour of the `denote-rename-file' command (and others
|
||
like it) is to ask for an affirmative answer as a final step before
|
||
changing the file name and, where relevant, inserting or updating the
|
||
corresponding front matter.
|
||
|
||
Specialized commands that build on top of `denote-rename-file' (or
|
||
related) may internally bind this user option to a non-nil value in
|
||
order to perform their operation (e.g. `denote-dired-rename-files' goes
|
||
through each marked Dired file, prompting for the information to use,
|
||
but carries out the renaming without asking for confirmation)."
|
||
:group 'denote
|
||
:type '(radio (const :tag "Disable all confirmations" nil)
|
||
(set :tag "Available confirmations" :greedy t
|
||
(const :tag "Add front matter" add-front-matter)
|
||
(const :tag "Rewrite front matter" rewrite-front-matter)
|
||
(const :tag "Modify file name" modify-file-name))))
|
||
|
||
(defcustom denote-excluded-directories-regexp nil
|
||
"Regular expression of directories to exclude from all operations.
|
||
Omit matching directories from file prompts and also exclude them
|
||
from all functions that check the contents of the variable
|
||
`denote-directory'. The regexp needs to match only the name of
|
||
the directory, not its full path.
|
||
|
||
File prompts are used by several commands, such as `denote-link'
|
||
and `denote-subdirectory'.
|
||
|
||
Functions that check for files include `denote-directory-files'
|
||
and `denote-directory-subdirectories'.
|
||
|
||
The match is performed with `string-match-p'."
|
||
:group 'denote
|
||
:package-version '(denote . "1.2.0")
|
||
:type '(choice (const :tag "Do not filter out anything (default)" nil)
|
||
(string :tag "Regular expression")))
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-excluded-keywords-regexp
|
||
'denote-keywords-to-not-infer-regexp
|
||
"4.2.0")
|
||
|
||
(defcustom denote-keywords-to-not-infer-regexp nil
|
||
"Regular expression of keywords to not infer.
|
||
Keywords are inferred from file names and provided at relevant
|
||
prompts as completion candidates when the user option
|
||
`denote-infer-keywords' is non-nil.
|
||
|
||
The match is performed with `string-match-p'."
|
||
:group 'denote
|
||
:package-version '(denote . "4.2.0")
|
||
:type '(choice (const :tag "Do not filter out anything (default)" nil)
|
||
(string :tag "Regular expression")))
|
||
|
||
(defcustom denote-excluded-files-regexp nil
|
||
"Regular expression of files that are excluded from Denote file prompts.
|
||
Files are provided for completion when using commands like `denote-link'
|
||
and `denote-open-or-create'.
|
||
|
||
The match is performed with `string-match-p' on the full file path."
|
||
:group 'denote
|
||
:package-version '(denote . "3.0.0")
|
||
:type '(choice (const :tag "Do not filter out anything (default)" nil)
|
||
(string :tag "Regular expression")))
|
||
|
||
(defcustom denote-after-new-note-hook nil
|
||
"Normal hook that runs after the `denote' command.
|
||
This also covers all convenience functions that call `denote'
|
||
internally, such as `denote-signature' and `denote-type' (check
|
||
the default value of the user option `denote-commands-for-new-notes')."
|
||
:group 'denote
|
||
:package-version '(denote . "2.1.0")
|
||
:link '(info-link "(denote) Standard note creation")
|
||
:type 'hook)
|
||
|
||
(defcustom denote-after-rename-file-hook nil
|
||
"Normal hook called after a succesful Denote rename operation.
|
||
This affects the behaviour of the commands `denote-rename-file',
|
||
`denote-dired-rename-files', `denote-rename-file-using-front-matter',
|
||
`denote-dired-rename-marked-files-with-keywords',
|
||
`denote-dired-rename-marked-files-using-front-matter',
|
||
`denote-keywords-add', `denote-keywords-remove', and any other
|
||
command that builds on top of them."
|
||
:group 'denote
|
||
:package-version '(denote . "2.3.0")
|
||
:link '(info-link "(denote) Renaming files")
|
||
:type 'hook)
|
||
|
||
(defcustom denote-region-after-new-note-functions nil
|
||
"Abnormal hook called after `denote-region'.
|
||
Functions in this hook are called with two arguments,
|
||
representing the beginning and end buffer positions of the region
|
||
that was inserted in the new note. These are called only if
|
||
`denote-region' is invoked while a region is active.
|
||
|
||
A common use-case is to call `org-insert-structure-template'
|
||
after a region is inserted. This case does not actually require
|
||
the aforementioned arguments, in which case the function can
|
||
simply declare them as ignored by prefixing the argument names
|
||
with an underscore. For example, the following will prompt for a
|
||
structure template as soon as `denote-region' is done:
|
||
|
||
(defun my-denote-region-org-structure-template (_beg _end)
|
||
(when (derived-mode-p \\='org-mode)
|
||
(activate-mark)
|
||
(call-interactively \\='org-insert-structure-template)))
|
||
|
||
(add-hook \\='denote-region-after-new-note-functions
|
||
#\\='my-denote-region-org-structure-template)"
|
||
:group 'denote
|
||
:package-version '(denote . "2.1.0")
|
||
:link '(info-link "(denote) Create a note with the region's contents")
|
||
:type 'hook)
|
||
|
||
(defvar denote-prompts-with-history-as-completion
|
||
'(denote-title-prompt denote-signature-prompt denote-files-matching-regexp-prompt denote-query-link-prompt)
|
||
"Prompts that conditionally perform completion against their history.
|
||
|
||
These are minibuffer prompts that ordinarily accept a free form string
|
||
input, as opposed to matching against a predefined set.
|
||
|
||
These prompts can optionally perform completion against their own
|
||
minibuffer history when the user option `denote-history-completion-in-prompts'
|
||
is set to a non-nil value.")
|
||
|
||
(defcustom denote-history-completion-in-prompts t
|
||
"Toggle history completion in all `denote-prompts-with-history-as-completion'.
|
||
|
||
When this user option is set to a non-nil value, use minibuffer history
|
||
entries as completion candidates in `denote-prompts-with-history-as-completion'.
|
||
Those will show previous inputs from their respective history as
|
||
possible values to select, either to (i) re-insert them verbatim or (ii)
|
||
with the intent to edit further (depending on the minibuffer user
|
||
interface, one can select a candidate with TAB without exiting the
|
||
minibuffer, as opposed to what RET normally does by selecting and
|
||
exiting).
|
||
|
||
When this user option is set to a nil value, all of the
|
||
`denote-prompts-with-history-as-completion' do not use minibuffer
|
||
completion: they just prompt for a string of characters. Their
|
||
history is still available through all the standard ways of retrieving
|
||
minibuffer history, such as with the command `previous-history-element'.
|
||
|
||
History completion still allows arbitrary values to be provided as
|
||
input: they do not have to match the available minibuffer completion
|
||
candidates.
|
||
|
||
Note that some prompts, like `denote-keywords-prompt', always use
|
||
minibuffer completion, due to the specifics of their data.
|
||
|
||
[ Consider enabling the built-in `savehist-mode' to persist minibuffer
|
||
histories between sessions.]
|
||
|
||
Also see `denote-prompts'."
|
||
:type 'boolean
|
||
:package-version '(denote . "2.3.0")
|
||
:group 'denote)
|
||
|
||
(defcustom denote-commands-for-new-notes
|
||
'(denote
|
||
denote-date
|
||
denote-subdirectory
|
||
denote-template
|
||
denote-type
|
||
denote-signature)
|
||
"List of commands for `denote-command-prompt' that create a new note.
|
||
These are used by commands such as `denote-open-or-create-with-command'
|
||
and `denote-link-after-creating-with-command'."
|
||
:group 'denote
|
||
:package-version '(denote . "2.1.0")
|
||
:link '(info-link "(denote) Choose which commands to prompt for")
|
||
:type '(repeat symbol))
|
||
|
||
(defcustom denote-buffer-name-prefix "[D] "
|
||
"String used as the prefix of all buffer names produced by Denote.
|
||
This includes the query links, backlinks, `denote-grep', `denote-dired'
|
||
buffers, as well as all the regular Denote files whose buffer name gets
|
||
updated when `denote-rename-buffer-mode' is enabled (that mode uses the
|
||
`denote-rename-buffer-format').
|
||
|
||
By default, the value of `denote-buffer-name-prefix' is \"[D] \".
|
||
Users can set it to any string they want, such as \"<Denote> \", or
|
||
even an empty string to not have any such prefix."
|
||
:type 'string
|
||
:package-version '(denote . "4.1.0")
|
||
:group 'denote)
|
||
|
||
(defcustom denote-file-name-slug-functions
|
||
'((identifier . identity)
|
||
(title . denote-sluggify-title)
|
||
(signature . denote-sluggify-signature)
|
||
(keyword . denote-sluggify-keyword))
|
||
"Specify the method Denote uses to format the components of the file name.
|
||
|
||
The value is an alist where each element is a cons cell of the
|
||
form (COMPONENT . METHOD).
|
||
|
||
- The COMPONENT is an unquoted symbol among `identifier', `title',
|
||
`signature', `keyword' (notice the absence of `s', see below),
|
||
which refers to the corresponding component of the file name.
|
||
|
||
- The METHOD is the function to be used to format the given
|
||
component. This function should take a string as its parameter
|
||
and return the string formatted for the file name. In the case
|
||
of the `keyword' component, the function receives a SINGLE
|
||
string representing a single keyword and return it formatted
|
||
for the file name. Joining the keywords together is handled by
|
||
Denote.
|
||
|
||
Note that the `keyword' function is also applied to the keywords
|
||
of the front matter.
|
||
|
||
By default, if a function is not specified for a component, we use
|
||
`identity', `denote-sluggify-title', `denote-sluggify-keyword' and
|
||
`denote-sluggify-signature'.
|
||
|
||
Remember that deviating from the default file-naming scheme of Denote
|
||
will make things harder to search in the future, as files can/will have
|
||
permutations that create uncertainty. The sluggification scheme and
|
||
concomitant restrictions we impose by default are there for a very good
|
||
reason: they are the distillation of years of experience. Here we give
|
||
you what you wish, but bear in mind it may not be what you need. You
|
||
have been warned."
|
||
:group 'denote
|
||
:package-version '(denote . "2.3.0")
|
||
:link '(info-link "(denote) User-defined sluggification of file name components")
|
||
:type '(alist :key (choice (const title)
|
||
(const signature)
|
||
(const keyword))
|
||
:value function))
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-link-button-action
|
||
'denote-open-link-function
|
||
"4.0.0")
|
||
|
||
(defcustom denote-open-link-function #'find-file-other-window
|
||
"Function to find the file of a Denote link.
|
||
|
||
The default value is `find-file-other-window', with `find-file' because
|
||
another common option. Users can provide a custom function which
|
||
behaves like the other two.
|
||
|
||
This is used in all non-Org buffers that have a link created by Denote.
|
||
Org has its own mechanism, which you can learn more about by reading the
|
||
documentation of the `org-open-at-point' command."
|
||
:group 'denote
|
||
:type '(choice (function :tag "Other window" find-file-other-window)
|
||
(function :tag "Current window" find-file)
|
||
(function :tag "Custom function"))
|
||
:package-version '(denote . "4.0.0"))
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-link-description-function
|
||
'denote-link-description-format
|
||
"4.0.0")
|
||
|
||
(defcustom denote-link-description-format #'denote-link-description-with-signature-and-title
|
||
"The format of a link description text.
|
||
This determines how `denote-link' and related functions create a link
|
||
description by default.
|
||
|
||
The value can be either a function or a string. If it is a function, it
|
||
is called with two arguments, the file and file type as a symbol among
|
||
`denote-file-types'. It should return a string representing the link
|
||
description. The default is a function that returns the active region
|
||
or the title of the note (with the signature if present).
|
||
|
||
For backward compatibility, the function can also take a single
|
||
parameter, the given file. In that case, it is responsible for figuring
|
||
out the file type in order to return the correct description.
|
||
|
||
If the value is a string, it treats specially the following specifiers:
|
||
|
||
- The %t is the Denote TITLE in the front matter or the file name.
|
||
- The %T is the Denote TITLE in the file name.
|
||
- The %i is the Denote IDENTIFIER of the file.
|
||
- The %I is the identifier converted to DAYNAME, DAYNUM MONTHNUM YEAR.
|
||
- The %d is the same as %i (DATE mnemonic).
|
||
- The %D is a \"do what I mean\" which behaves the same as %t and if
|
||
that returns nothing, it falls back to %I, then %i.
|
||
- The %s is the Denote SIGNATURE of the file.
|
||
- The %k is the Denote KEYWORDS of the file.
|
||
- The %% is a literal percent sign.
|
||
|
||
In addition, the following flags are available for each of the specifiers:
|
||
|
||
- 0 :: Pad to the width, if given, with zeros instead of spaces.
|
||
- - :: Pad to the width, if given, on the right instead of the left.
|
||
- < :: Truncate to the width and precision, if given, on the left.
|
||
- > :: Truncate to the width and precision, if given, on the right.
|
||
- ^ :: Convert to upper case.
|
||
- _ :: Convert to lower case.
|
||
|
||
When combined all together, the above are written thus:
|
||
|
||
%<flags><width><precision>SPECIFIER-CHARACTER
|
||
|
||
Any other text in the string it taken as-is. Users may want, for
|
||
example, to include some text that makes Denote links stand out, such as
|
||
a [D] prefix.
|
||
|
||
If the region is active, its text is used as the link's description."
|
||
:type '(choice
|
||
(string :tag "String with treats format specifiers specially")
|
||
(function :tag "Custom function like `denote-link-description-with-signature-and-title'"))
|
||
:package-version '(denote . "4.0.0")
|
||
:group 'denote)
|
||
|
||
;;;; Main variables
|
||
|
||
;; For character classes, evaluate: (info "(elisp) Char Classes")
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-id-format
|
||
'denote-date-identifier-format
|
||
"4.1.0")
|
||
|
||
(defconst denote-date-identifier-format "%Y%m%dT%H%M%S"
|
||
"Format of ID prefix of a note's filename.
|
||
The note's ID is derived from the date and time of its creation.")
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-id-regexp
|
||
'denote-date-identifier-regexp
|
||
"4.1.0")
|
||
|
||
(defconst denote-date-identifier-regexp "\\([0-9]\\{8\\}\\)\\(T[0-9]\\{6\\}\\)"
|
||
"Regular expression to match `denote-date-identifier-format'.")
|
||
|
||
(defconst denote-identifier-regexp "@@\\([^.]+?\\)\\(==.*\\|--.*\\|__.*\\|@@.*\\|\\..*\\)*$"
|
||
"Regular expression to match the IDENTIFIER field in a file name.")
|
||
|
||
(defconst denote-signature-regexp "==\\([^.]+?\\)\\(==.*\\|--.*\\|__.*\\|@@.*\\|\\..*\\)*$"
|
||
"Regular expression to match the SIGNATURE field in a file name.")
|
||
|
||
(defconst denote-title-regexp "--\\([^.]+?\\)\\(==.*\\|__.*\\|@@.*\\|\\..*\\)*$"
|
||
"Regular expression to match the TITLE field in a file name.")
|
||
|
||
(defconst denote-keywords-regexp "__\\([^.]+?\\)\\(==.*\\|--.*\\|__.*\\|@@.*\\|\\..*\\)*$"
|
||
"Regular expression to match the KEYWORDS field in a file name.")
|
||
|
||
(make-obsolete-variable
|
||
'denote-excluded-punctuation-extra-regexp
|
||
'denote-file-name-slug-functions
|
||
"4.0.0")
|
||
|
||
;;;; File helper functions
|
||
|
||
(defun denote-get-completion-table (candidates &rest metadata)
|
||
"Return completion table with CANDIDATES and METADATA.
|
||
CANDIDATES is a list of strings. METADATA is described in
|
||
`completion-metadata'."
|
||
(lambda (string pred action)
|
||
(if (eq action 'metadata)
|
||
(cons 'metadata metadata)
|
||
(complete-with-action action candidates string pred))))
|
||
|
||
(defun denote--default-directory-is-silo-p ()
|
||
"Return path to silo if `default-directory' is a silo."
|
||
(when-let* ((dir-locals (dir-locals-find-file default-directory))
|
||
((alist-get 'denote-directory dir-local-variables-alist)))
|
||
(cond
|
||
((listp dir-locals)
|
||
(car dir-locals))
|
||
((stringp dir-locals)
|
||
dir-locals))))
|
||
|
||
(defun denote-directories--make-paths (directories)
|
||
"Call `make-directory' on each element of DIRECTORIES unless it exists.
|
||
Make any parent directories as well."
|
||
(dolist (directory directories)
|
||
(unless (file-directory-p directory)
|
||
(make-directory directory :parents))))
|
||
|
||
(defun denote-directories--get-paths (directory-or-directories)
|
||
"Return DIRECTORY-OR-DIRECTORIES as a list of absolute paths."
|
||
(unless (or (seq-every-p #'stringp directory-or-directories)
|
||
(stringp directory-or-directories))
|
||
(error "The `%S' has to be a string or a list of strings" directory-or-directories))
|
||
(let ((get-dir (lambda (directory) (file-name-as-directory (expand-file-name directory)))))
|
||
(if (listp directory-or-directories)
|
||
(mapcar get-dir directory-or-directories)
|
||
(list (funcall get-dir directory-or-directories)))))
|
||
|
||
(defun denote-directories ()
|
||
"Return path of variable `denote-directory' as a proper directory.
|
||
If the variable `denote-directory' is set to a list of file paths,
|
||
return the list with each element expanded to be a directory. Create
|
||
any directories and their parents, if needed.
|
||
|
||
Custom Lisp code can `let' bind the variable `denote-directory'
|
||
to override what this function returns."
|
||
(if-let* (((or (eq denote-directory 'default-directory) (eq denote-directory 'local)))
|
||
(silo-dir (denote--default-directory-is-silo-p)))
|
||
(progn
|
||
(display-warning
|
||
'denote
|
||
"Silo value must be a string; `local' or `default-directory' are obsolete"
|
||
:error)
|
||
(list silo-dir))
|
||
(let ((denote-directories (denote-directories--get-paths denote-directory)))
|
||
(denote-directories--make-paths denote-directories)
|
||
denote-directories)))
|
||
|
||
(defun denote-has-single-denote-directory-p ()
|
||
"Return non-nil if the variable `denote-directory' is a single item."
|
||
(not (cdr (denote-directories))))
|
||
|
||
(defun denote--get-common-root-directory (directories)
|
||
"Return common root directory among DIRECTORIES.
|
||
DIRECTORIES are absolute file system paths."
|
||
(cond
|
||
((or (null directories)
|
||
(not (seq-every-p
|
||
(lambda (d)
|
||
(and (file-name-absolute-p d)
|
||
(not (string-prefix-p "~" d))))
|
||
directories)))
|
||
(error "DIRECTORIES must be a list of absolute filesystem paths, not `%S'" directories))
|
||
((null (cdr directories))
|
||
(car directories))
|
||
((let* ((parts (mapcar
|
||
(lambda (dir)
|
||
(split-string dir "/" :omit-nulls))
|
||
directories))
|
||
(common-prefix (car parts)))
|
||
(dolist (part (cdr parts))
|
||
(let ((new-common-prefix nil))
|
||
(while (and common-prefix
|
||
part
|
||
(string= (car common-prefix) (car part)))
|
||
(push (car common-prefix) new-common-prefix)
|
||
(setq common-prefix (cdr common-prefix))
|
||
(setq part (cdr part)))
|
||
(setq common-prefix (nreverse new-common-prefix))))
|
||
(if common-prefix
|
||
(format "/%s/" (mapconcat #'identity common-prefix "/"))
|
||
"/")))))
|
||
|
||
(defun denote-directories-get-common-root ()
|
||
"Get the common root directory of `denote-directories'."
|
||
(denote--get-common-root-directory (denote-directories)))
|
||
|
||
(defun denote-directory ()
|
||
"Return the `car' of `denote-directories'.
|
||
Unless this is definitely what you need, use the `denote-directories'
|
||
instead. Also see `denote-directories-get-common-root'."
|
||
(car (denote-directories)))
|
||
|
||
(make-obsolete 'denote-directory 'denote-directories "4.1.0")
|
||
|
||
(defvar denote-generate-identifier-automatically t
|
||
"Make creation and renaming commands automatically create and identifier.
|
||
|
||
This applies when a note is created or renamed. The default is to
|
||
always create an identifier automatically.
|
||
|
||
Valid values are: t, nil, `on-creation', and `on-rename'.")
|
||
|
||
(defvar denote-accept-nil-date nil
|
||
"Make creation and renaming commands use `current-time' when date is nil.")
|
||
|
||
(defun denote--user-error-if-not-major-mode (mode)
|
||
"Signal `user-error' is MODE is not `derived-mode-p'."
|
||
(unless (derived-mode-p mode)
|
||
(user-error "Only use this command in a `%s' buffer" mode)))
|
||
|
||
;;;;; Sluggification functions
|
||
|
||
(defun denote-slug-keep-only-ascii (str)
|
||
"Remove all non-ASCII characters from STR and replace them with spaces.
|
||
This is useful as a helper function to construct
|
||
`denote-file-name-slug-functions'."
|
||
(let* ((ascii-range (seq-map
|
||
(lambda (character)
|
||
(if (and (>= character 33) (<= character 126))
|
||
character
|
||
32)) ; empty space
|
||
str))
|
||
(characters (seq-filter #'characterp ascii-range)))
|
||
(mapconcat #'string characters)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--slug-hyphenate
|
||
'denote-slug-hyphenate
|
||
"4.0.0")
|
||
|
||
(defun denote-slug-hyphenate (str)
|
||
"Replace spaces and underscores with hyphens in STR.
|
||
Also replace multiple hyphens with a single one and remove any
|
||
leading and trailing hyphen."
|
||
(replace-regexp-in-string
|
||
"^-\\|-$" ""
|
||
(replace-regexp-in-string
|
||
"-\\{2,\\}" "-"
|
||
(replace-regexp-in-string "_\\|\s+" "-" str))))
|
||
|
||
(defun denote-slug-put-equals (str)
|
||
"Replace spaces and underscores with equals signs in STR.
|
||
Also replace multiple equals signs with a single one and remove
|
||
any leading and trailing signs."
|
||
(replace-regexp-in-string
|
||
"^=\\|=$" ""
|
||
(replace-regexp-in-string
|
||
"=\\{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))
|
||
|
||
(defun denote--trim-right-token-characters (str component)
|
||
"Remove =, -, _ and @ from the end of STR.
|
||
The removal is done only if necessary according to COMPONENT."
|
||
(if (eq component 'title)
|
||
(string-trim-right str "[=@_]+")
|
||
(string-trim-right str "[=@_-]+")))
|
||
|
||
(defun denote--replace-consecutive-token-characters (str component)
|
||
"Replace consecutive characters with a single one in STR.
|
||
Hyphens, underscores, equal signs and at signs are replaced with
|
||
a single one in str, if necessary according to COMPONENT."
|
||
(let ((str (replace-regexp-in-string
|
||
"_\\{2,\\}" "_"
|
||
(replace-regexp-in-string
|
||
"=\\{2,\\}" "="
|
||
(replace-regexp-in-string
|
||
"@\\{2,\\}" "@" str)))))
|
||
;; -- are allowed in titles when the default sluggification is disabled
|
||
(if (eq component 'title)
|
||
str
|
||
(replace-regexp-in-string
|
||
"-\\{2,\\}" "-" str))))
|
||
|
||
(defun denote-sluggify-and-apply-rules (component str)
|
||
"Make STR an appropriate slug for file name COMPONENT.
|
||
|
||
Apply the function specified in `denote-file-name-slug-function' to
|
||
COMPONENT which is one of `identifier', `title', `signature', `keyword'.
|
||
If the resulting string still contains consecutive -, _, =, or @, they
|
||
are replaced by a single occurence of the character, if necessary
|
||
according to COMPONENT. If COMPONENT is `keyword', remove underscores
|
||
from STR as they are used as the keywords separator in file names.
|
||
|
||
Also enforce the rules of the file-naming scheme."
|
||
(let* ((slug-function (alist-get component denote-file-name-slug-functions))
|
||
(str-slug (pcase component
|
||
('title (funcall (or slug-function #'denote-sluggify-title) str))
|
||
('keyword (replace-regexp-in-string
|
||
"_" ""
|
||
(funcall (or slug-function #'denote-sluggify-keyword) str)))
|
||
('identifier (denote--valid-identifier (funcall (or slug-function #'identity) str)))
|
||
('signature (funcall (or slug-function #'denote-sluggify-signature) str)))))
|
||
(thread-first
|
||
(denote--remove-dot-characters str-slug)
|
||
(denote--replace-consecutive-token-characters component)
|
||
(denote--trim-right-token-characters component))))
|
||
|
||
(defalias 'denote-sluggify 'denote-sluggify-and-apply-rules
|
||
"Alias for the function `denote-sluggify-and-apply-rules'.")
|
||
|
||
(defun denote-sluggify-title (str)
|
||
"Make STR an appropriate slug for title."
|
||
(downcase
|
||
(denote-slug-hyphenate
|
||
(replace-regexp-in-string "[][{}!@#$%^&*()+'\"?,.\|;:~`‘’“”/=]*" "" str))))
|
||
|
||
(defun denote-sluggify-signature (str)
|
||
"Make STR an appropriate slug for signature."
|
||
(downcase
|
||
(denote-slug-put-equals
|
||
(replace-regexp-in-string "[][{}!@#$%^&*()+'\"?,.\|;:~`‘’“”/-]*" "" str))))
|
||
|
||
(defun denote-sluggify-keyword (str)
|
||
"Sluggify STR while joining separate words."
|
||
(downcase
|
||
(replace-regexp-in-string "[][{}!@#$%^&*()+'\"?,.\|;:~`‘’“”/_ =-]*" "" str)))
|
||
|
||
(defun denote-sluggify-keywords-and-apply-rules (keywords)
|
||
"Sluggify KEYWORDS, which is a list of strings."
|
||
(mapcar (lambda (keyword)
|
||
(denote-sluggify-and-apply-rules 'keyword keyword))
|
||
keywords))
|
||
|
||
(defalias 'denote-sluggify-keywords 'denote-sluggify-keywords-and-apply-rules
|
||
"Alias for the function `denote-sluggify-keywords-and-apply-rules'.")
|
||
|
||
;;;;; Common helper functions
|
||
|
||
(defun denote--file-empty-p (file)
|
||
"Return non-nil if FILE is empty."
|
||
(zerop (or (file-attribute-size (file-attributes file)) 0)))
|
||
|
||
(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))
|
||
|
||
(defun denote-file-has-supported-extension-p (file)
|
||
"Return non-nil if FILE has supported extension.
|
||
Also account for the possibility of an added .gpg suffix. Supported
|
||
extensions are those implied by the variable `denote-file-type'."
|
||
(seq-some (lambda (e)
|
||
(string-suffix-p e file))
|
||
(denote-file-type-extensions-with-encryption)))
|
||
|
||
(defun denote-file-is-in-denote-directory-p (file)
|
||
"Return non-nil if FILE is in the variable `denote-directory'."
|
||
(seq-some
|
||
(lambda (d)
|
||
(string-prefix-p d (expand-file-name file)))
|
||
(denote-directories)))
|
||
|
||
(defun denote-filename-is-note-p (filename)
|
||
"Return non-nil if FILENAME is a valid name for a Denote note.
|
||
For our purposes, its path must be part of the variable
|
||
`denote-directory', it must have a Denote identifier in its name, and
|
||
use one of the extensions implied by the variable `denote-file-type'."
|
||
(and (denote-file-is-in-denote-directory-p filename)
|
||
(denote-file-has-identifier-p filename)
|
||
(denote-file-has-supported-extension-p filename)))
|
||
|
||
(defun denote-file-is-note-p (file)
|
||
"Return non-nil if FILE is an actual Denote note.
|
||
For our purposes, a note must satisfy `file-regular-p' and
|
||
`denote-filename-is-note-p'."
|
||
(and (file-regular-p file) (denote-filename-is-note-p file)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-filename-is-note-p
|
||
'denote-file-has-denoted-filename-p
|
||
"4.1.0")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-file-is-note-p
|
||
'denote-file-has-denoted-filename-p
|
||
"4.1.0")
|
||
|
||
(defun denote-file-has-denoted-filename-p (file)
|
||
"Return non-nil if FILE respects the Denote file-naming scheme.
|
||
|
||
Test the rules of Denote's file-naming scheme, notwithstanding the
|
||
`denote-file-name-slug-functions'. As such, ignore sluggification, by
|
||
removing all file name components and validating what remains."
|
||
(let* ((initial-filename (file-name-nondirectory file))
|
||
(filename initial-filename)
|
||
(title (denote-retrieve-filename-title file))
|
||
(keywords-string (denote-retrieve-filename-keywords file))
|
||
(signature (denote-retrieve-filename-signature file))
|
||
(identifier (denote-retrieve-filename-identifier file)))
|
||
(when title
|
||
(setq filename (replace-regexp-in-string (concat "\\(--" (regexp-quote title) "\\).*\\'") "" filename nil nil 1)))
|
||
(when keywords-string
|
||
(setq filename (replace-regexp-in-string (concat "\\(__" (regexp-quote keywords-string) "\\).*\\'") "" filename nil nil 1)))
|
||
(when signature
|
||
(setq filename (replace-regexp-in-string (concat "\\(==" (regexp-quote signature) "\\).*\\'") "" filename nil nil 1)))
|
||
(when identifier
|
||
(if (string-match-p "@@" filename)
|
||
(setq filename (replace-regexp-in-string (concat "\\(@@" (regexp-quote identifier) "\\).*\\'") "" filename nil nil 1))
|
||
(setq filename (replace-regexp-in-string (concat "\\(" (regexp-quote identifier) "\\).*\\'") "" filename nil nil 1))))
|
||
;; What remains should be the empty string or the file extension.
|
||
(and (not (string-prefix-p "." initial-filename))
|
||
(or (string-empty-p filename)
|
||
(string-prefix-p "." filename)))))
|
||
|
||
(defun denote-file-has-signature-p (file)
|
||
"Return non-nil if FILE has a Denote identifier."
|
||
(denote-retrieve-filename-signature file))
|
||
|
||
(defun denote-file-is-writable-and-supported-p (file)
|
||
"Return non-nil if FILE is writable and has supported extension."
|
||
;; We do not want to test that the file is regular (exists) because we want
|
||
;; this function to return t on files that are still unsaved.
|
||
(and (file-writable-p file)
|
||
(denote-file-has-supported-extension-p file)))
|
||
|
||
(defun denote--get-file-name-relative-to-directories (file directories)
|
||
"Return FILE relative to one of the DIRECTORIES."
|
||
(unless (file-name-absolute-p file)
|
||
(error "The file `%s' is not absolute" file))
|
||
(when-let* ((directory (seq-find
|
||
(lambda (d)
|
||
(string-prefix-p d file))
|
||
directories)))
|
||
(substring-no-properties file (length directory))))
|
||
|
||
(defun denote-get-file-name-relative-to-denote-directory (file)
|
||
"Return FILE relative to the variable `denote-directory'."
|
||
(denote--get-file-name-relative-to-directories file (denote-directories)))
|
||
|
||
(defun denote-extract-id-from-string (string)
|
||
"Return existing Denote identifier in STRING, else nil."
|
||
(when (string-match denote-date-identifier-regexp string)
|
||
(match-string-no-properties 0 string)))
|
||
|
||
(defun denote--exclude-directory-regexp-p (file)
|
||
"Return non-nil if FILE matches `denote-excluded-directories-regexp'."
|
||
(and (stringp denote-excluded-directories-regexp)
|
||
(string-match-p denote-excluded-directories-regexp file)))
|
||
|
||
(defun denote--directory-all-files-recursively ()
|
||
"Return list of all files in variable `denote-directories'.
|
||
Avoids traversing dotfiles (unconditionally) and whatever matches
|
||
`denote-excluded-directories-regexp'."
|
||
(let* ((directories (denote-directories))
|
||
(predicate-fn
|
||
(lambda (file)
|
||
(let ((rel (denote--get-file-name-relative-to-directories file directories)))
|
||
(cond
|
||
((string-match-p "\\`\\." rel) nil)
|
||
((string-match-p "/\\." rel) nil)
|
||
((denote--exclude-directory-regexp-p rel) nil)
|
||
((file-readable-p file)))))))
|
||
(apply #'append
|
||
(mapcar
|
||
(lambda (directory)
|
||
(directory-files-recursively
|
||
directory
|
||
directory-files-no-dot-files-regexp
|
||
:include-directories
|
||
predicate-fn
|
||
:follow-symlinks))
|
||
directories))))
|
||
|
||
(defun denote--file-excluded-p (file)
|
||
"Return non-file if FILE matches `denote-excluded-files-regexp'."
|
||
(and denote-excluded-files-regexp
|
||
(string-match-p denote-excluded-files-regexp file)))
|
||
|
||
(defun denote--directory-get-files ()
|
||
"Return list with full path of valid files in variable `denote-directory'.
|
||
Consider files that satisfy `denote-file-has-denoted-filename-p' and
|
||
are not backups."
|
||
(seq-filter
|
||
(lambda (file)
|
||
(and (file-regular-p file)
|
||
(denote-file-has-denoted-filename-p file)
|
||
(not (denote--file-excluded-p file))
|
||
(not (backup-file-name-p file))))
|
||
(denote--directory-all-files-recursively)))
|
||
|
||
(make-obsolete-variable
|
||
'denote-directory-get-files-function
|
||
"advanced users should write an advice for `denote-directory-files'"
|
||
"4.2.0")
|
||
|
||
;; The HAS-IDENTIFIER is there because we support cases where files do
|
||
;; not have an identifier.
|
||
(defun denote-directory-files (&optional files-matching-regexp omit-current text-only exclude-regexp has-identifier)
|
||
"Return list of absolute file paths in variable `denote-directory'.
|
||
Files that match `denote-excluded-files-regexp' are excluded from the
|
||
list.
|
||
|
||
Files only need to have an identifier. The return value may thus
|
||
include file types that are not implied by the variable
|
||
`denote-file-type'.
|
||
|
||
With optional FILES-MATCHING-REGEXP, restrict files to those
|
||
matching the given regular expression.
|
||
|
||
With optional OMIT-CURRENT as a non-nil value, do not include the
|
||
current Denote file in the returned list.
|
||
|
||
With optional TEXT-ONLY as a non-nil value, limit the results to
|
||
text files that satisfy `denote-file-has-supported-extension-p'.
|
||
|
||
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.
|
||
|
||
With optional HAS-IDENTIFIER as a non-nil value, limit the results to
|
||
files that have an identifier."
|
||
(let ((files (denote--directory-get-files)))
|
||
(when (and omit-current buffer-file-name (denote-file-has-identifier-p buffer-file-name))
|
||
(setq files (delete buffer-file-name files)))
|
||
(when files-matching-regexp
|
||
(let ((dirs (denote-directories)))
|
||
(setq files (seq-filter
|
||
(lambda (f)
|
||
(string-match-p files-matching-regexp (denote--get-file-name-relative-to-directories f dirs)))
|
||
files))))
|
||
(when text-only
|
||
(setq files (seq-filter #'denote-file-has-supported-extension-p files)))
|
||
(when has-identifier
|
||
(setq files (seq-filter #'denote-file-has-identifier-p files)))
|
||
(when exclude-regexp
|
||
(setq files (seq-remove
|
||
(lambda (file)
|
||
(string-match-p exclude-regexp file))
|
||
files)))
|
||
files))
|
||
|
||
(defun denote-directory-subdirectories ()
|
||
"Return list of subdirectories in variable `denote-directory'.
|
||
Omit dotfiles (such as .git) unconditionally. Also exclude
|
||
whatever matches `denote-excluded-directories-regexp'."
|
||
(let ((dirs (denote-directories)))
|
||
(seq-remove
|
||
(lambda (filename)
|
||
(let ((rel (denote--get-file-name-relative-to-directories filename dirs)))
|
||
(or (not (file-directory-p filename))
|
||
(string-match-p "\\`\\." rel)
|
||
(string-match-p "/\\." rel)
|
||
(denote--exclude-directory-regexp-p rel))))
|
||
(denote--directory-all-files-recursively))))
|
||
|
||
;; TODO 2023-01-24: Perhaps there is a good reason to make this a user
|
||
;; option, but I am keeping it as a generic variable for now.
|
||
(defvar denote-encryption-file-extensions '(".gpg" ".age")
|
||
"List of strings specifying file extensions for encryption.")
|
||
|
||
(defun denote-file-type-extensions-with-encryption ()
|
||
"Derive `denote-file-type-extensions' plus `denote-encryption-file-extensions'."
|
||
(let ((file-extensions (denote-file-type-extensions))
|
||
all)
|
||
(dolist (ext file-extensions)
|
||
(dolist (enc denote-encryption-file-extensions)
|
||
(push (concat ext enc) all)))
|
||
(append file-extensions all)))
|
||
|
||
(defun denote-get-file-extension (file)
|
||
"Return extension of FILE with dot included.
|
||
Account for `denote-encryption-file-extensions'. In other words,
|
||
return something like .org.gpg if it is part of the file, else
|
||
return .org."
|
||
(let ((outer-extension (file-name-extension file :period)))
|
||
(if-let* (((member outer-extension denote-encryption-file-extensions))
|
||
(file (file-name-sans-extension file))
|
||
(inner-extension (file-name-extension file :period)))
|
||
(concat inner-extension outer-extension)
|
||
outer-extension)))
|
||
|
||
(defun denote-get-file-extension-sans-encryption (file)
|
||
"Return extension of FILE with dot included and without the encryption part.
|
||
Build on top of `denote-get-file-extension' though always return
|
||
something like .org even if the actual file extension is
|
||
.org.gpg."
|
||
(let ((extension (denote-get-file-extension file)))
|
||
(if (string-match (regexp-opt denote-encryption-file-extensions) extension)
|
||
(substring extension 0 (match-beginning 0))
|
||
extension)))
|
||
|
||
(defun denote-get-path-by-id (id)
|
||
"Return absolute path of ID string in `denote-directory-files'."
|
||
(let ((files
|
||
(seq-filter
|
||
(lambda (file)
|
||
(string= id (denote-retrieve-filename-identifier file)))
|
||
(denote-directory-files nil nil nil nil :has-identifier))))
|
||
(if (length< files 2)
|
||
(car files)
|
||
(seq-find
|
||
(lambda (file)
|
||
(let ((file-extension (denote-get-file-extension-sans-encryption file)))
|
||
(and (denote-file-has-supported-extension-p file)
|
||
(or (string= (denote--file-extension denote-file-type)
|
||
file-extension)
|
||
(string= ".org" file-extension)
|
||
(member file-extension (denote-file-type-extensions))))))
|
||
files))))
|
||
|
||
(defun denote-get-relative-path-by-id (id &optional directory)
|
||
"Return relative path of ID string in `denote-directory-files'.
|
||
The path is relative to DIRECTORY (default: ‘default-directory’)."
|
||
(when-let* ((path (denote-get-path-by-id id)))
|
||
(file-relative-name path directory)))
|
||
|
||
(defvar denote-file-history nil
|
||
"Minibuffer history of `denote-file-prompt'.")
|
||
|
||
(defalias 'denote--file-history 'denote-file-history
|
||
"Compatibility alias for `denote-file-history'.")
|
||
|
||
(defvar denote-file-prompt-latest-input nil
|
||
"Latest input passed to `denote-file-prompt'.
|
||
This is used for retrieving a value that is used to set a new default at
|
||
the title prompt of `denote-open-or-create' and related commands.")
|
||
|
||
(defvar denote-file-prompt-use-files-matching-regexp nil
|
||
"The `denote-file-prompt' FILES-MATCHING-REGEXP value.
|
||
Only ever `let' bind this, otherwise the restriction will always be
|
||
there.")
|
||
|
||
(defun denote-file-prompt-group (file transform)
|
||
"Retun group of FILE if TRANSFORM is non-nil, per `completion-metadata'."
|
||
(cond
|
||
;; FIXME 2025-12-16: Why do we not get highlights of matched
|
||
;; visible words? It works if I just return FILE.
|
||
(transform (or (denote-retrieve-filename-title file) file))
|
||
((file-name-directory file))
|
||
((file-name-extension file))
|
||
(t "Other files")))
|
||
|
||
(defun denote-file-prompt--format-identifier (file)
|
||
"Return identifier of FILE for `denote-file-prompt-affixate'."
|
||
(when-let* ((identifier (denote-retrieve-filename-identifier file))
|
||
(date-or-id (or (ignore-errors (denote-id-to-date identifier)) identifier)))
|
||
(format "%s " (propertize date-or-id 'face 'completions-annotations))))
|
||
|
||
(defun denote-file-prompt--format-keywords-and-signature (file)
|
||
"Return keywords and signature of FILE for `denote-file-prompt-affixate'."
|
||
(let ((keywords (denote-retrieve-filename-keywords file))
|
||
(signature (denote-retrieve-filename-signature file)))
|
||
(when-let* ((combined (cond
|
||
((and keywords signature) (concat "=" signature " " keywords))
|
||
(keywords)
|
||
(signature))))
|
||
(format " %s%s"
|
||
(if (eq completions-format 'one-column)
|
||
(propertize " " 'display '(space :align-to 90))
|
||
" ")
|
||
(propertize combined 'face 'completions-annotations)))))
|
||
|
||
(defun denote-file-prompt-affixate (files)
|
||
"Affixate FILES.
|
||
Use the identifier as a prefix and the keywords as a suffix."
|
||
(mapcar
|
||
(lambda (file)
|
||
(list
|
||
file
|
||
(or (denote-file-prompt--format-identifier file) "")
|
||
(or (denote-file-prompt--format-keywords-and-signature file) "")))
|
||
files))
|
||
|
||
(defun denote-file-prompt-sort (files)
|
||
"Sort FILES for `denote-file-prompt', per `completion-metadata'."
|
||
(sort files #'denote-sort-modified-time-greaterp))
|
||
|
||
(defvar denote-file-prompt-extra-metadata
|
||
(list
|
||
;; NOTE 2025-12-15: If we use the `file' category, then we are
|
||
;; subject to the `completion-category-overrides'. This is a
|
||
;; problem because the user will want to, for example, sort
|
||
;; directories before files, but then we cannot have our sort here.
|
||
(cons 'category 'denote-file)
|
||
(cons 'group-function #'denote-file-prompt-group)
|
||
(cons 'affixation-function #'denote-file-prompt-affixate)
|
||
(cons 'display-sort-function #'denote-file-prompt-sort))
|
||
"Extra `completion-metadata' for the `denote-file-prompt'.")
|
||
|
||
(with-eval-after-load 'all-the-icons
|
||
(cl-defmethod all-the-icons-completion-get-icon (cand (_cat (eql denote-file)))
|
||
"Return the icon for the candidate CAND of completion category denote-file."
|
||
(all-the-icons-completion-get-icon cand 'file)))
|
||
|
||
(with-eval-after-load 'nerd-icons
|
||
(cl-defmethod nerd-icons-completion-get-icon (cand (_cat (eql denote-file)))
|
||
"Return the icon for the candidate CAND of completion category denote-file."
|
||
(nerd-icons-completion-get-icon cand 'file)))
|
||
|
||
(defun denote-file-prompt (&optional files-matching-regexp prompt-text no-require-match has-identifier)
|
||
"Prompt for file in variable `denote-directory'.
|
||
Files that match `denote-excluded-files-regexp' are excluded from the
|
||
list.
|
||
|
||
With optional FILES-MATCHING-REGEXP, filter the candidates per
|
||
the given regular expression.
|
||
|
||
With optional PROMPT-TEXT, use it instead of the default call to
|
||
select a file.
|
||
|
||
With optional NO-REQUIRE-MATCH, accept the given input as-is.
|
||
|
||
With optional HAS-IDENTIFIER, only show candidates that have an
|
||
identifier.
|
||
|
||
Return the absolute path to the matching file."
|
||
(let* ((roots (denote-directories))
|
||
(single-dir-p (null (cdr roots)))
|
||
;; Some external program may use `default-directory' with the
|
||
;; relative file paths of the completion candidates.
|
||
(default-directory (if single-dir-p
|
||
(car roots)
|
||
(denote-directories-get-common-root)))
|
||
(files (denote-directory-files
|
||
(or denote-file-prompt-use-files-matching-regexp files-matching-regexp)
|
||
:omit-current nil nil has-identifier))
|
||
(relative-files (if single-dir-p
|
||
(mapcar
|
||
(lambda (file)
|
||
(denote--get-file-name-relative-to-directories file roots))
|
||
files)
|
||
files))
|
||
(prompt (if single-dir-p
|
||
(format "%s: " (or prompt-text "Select FILE"))
|
||
(format "%s in %s: "
|
||
(or prompt-text "Select FILE")
|
||
(propertize default-directory 'face 'denote-faces-prompt-current-name))))
|
||
(input (completing-read
|
||
prompt
|
||
(apply 'denote-get-completion-table relative-files denote-file-prompt-extra-metadata)
|
||
nil (unless no-require-match t)
|
||
nil 'denote-file-history))
|
||
(absolute-file (if single-dir-p
|
||
(expand-file-name input default-directory)
|
||
input)))
|
||
;; NOTE: This block is executed when no-require-match is t. It is useful
|
||
;; for commands such as `denote-open-or-create' or similar.
|
||
(unless (file-exists-p absolute-file)
|
||
(setq denote-file-prompt-latest-input input)
|
||
(setq denote-file-history (delete input denote-file-history)))
|
||
;; NOTE: We must always return an absolute path, even if it does not
|
||
;; exist, because callers expect one. They handle a non-existent file
|
||
;; appropriately.
|
||
absolute-file))
|
||
|
||
(defun denote-format-buffer-name (string &optional special)
|
||
"Use STRING to return a Denote buffer name with `denote-buffer-name-prefix'.
|
||
With optional SPECIAL, wrap the name in asterisks, signifying that this
|
||
is a special buffer."
|
||
(format (if special "*%s%s*" "%s%s") denote-buffer-name-prefix string))
|
||
|
||
;;;; 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 random last-modified)
|
||
"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 . "4.0.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 . "4.0.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)
|
||
(const :tag "Random order" random)
|
||
(const :tag "Last modified" last-modified))
|
||
:package-version '(denote . "4.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)
|
||
|
||
(defun denote-sort-random (elements)
|
||
"Shuffle ELEMENTS of list randomly."
|
||
(let* ((elements (copy-sequence elements))
|
||
(shuffled-list nil))
|
||
(while elements
|
||
(let* ((list-length (length elements))
|
||
(element (nth (random list-length) elements)))
|
||
(setq elements (delq element elements))
|
||
(push element shuffled-list)))
|
||
shuffled-list))
|
||
|
||
(defun denote-sort-modified-time-greaterp (file1 file2)
|
||
"Return non-nil if FILE1 modified time is greater than that of FILE2."
|
||
(let* ((attributes1 (file-attributes file1))
|
||
(mod-time1-raw (file-attribute-modification-time attributes1))
|
||
(mod-time1-seconds (time-to-seconds mod-time1-raw))
|
||
(attributes2 (file-attributes file2))
|
||
(mod-time2-raw (file-attribute-modification-time attributes2))
|
||
(mod-time2-seconds (time-to-seconds mod-time2-raw)))
|
||
;; NOTE 2025-06-23: We normally sort using a "less" approach, but
|
||
;; here we want to capture the semantics of "last modified"
|
||
;; without relying on a reverse sort.
|
||
(> mod-time1-seconds mod-time2-seconds)))
|
||
|
||
;;;###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)))
|
||
(if (eq component 'random)
|
||
(denote-sort-random files)
|
||
(let* ((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)
|
||
('last-modified #'denote-sort-modified-time-greaterp)))
|
||
(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-annotate (component)
|
||
"Annotate COMPONENT for `denote-sort-component-prompt'."
|
||
(when-let* ((text (pcase component
|
||
("title" "The title of the file name")
|
||
("keywords" "The keywords of the file name")
|
||
("signature" "The signature of the file name")
|
||
("identifier" "The identifier of the file name")
|
||
("random" "Random file sort")
|
||
("last-modified" "File last modification time"))))
|
||
(format "%s-- %s"
|
||
(propertize " " 'display '(space :align-to 15))
|
||
(propertize text 'face 'completions-annotations))))
|
||
|
||
(defun denote-sort-component-prompt ()
|
||
"Prompt for sorting key among `denote-sort-components'."
|
||
(let ((default (car denote-sort-component-history))
|
||
(completion-extra-properties (list :annotation-function #'denote-sort-annotate)))
|
||
(intern
|
||
(completing-read
|
||
(format-prompt "Sort by file name component" default)
|
||
(denote-get-completion-table denote-sort-components '(category . denote-sort-component))
|
||
nil t 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)))
|
||
|
||
(defun denote-sort-dired--prompts ()
|
||
"Return list of prompt symbols 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)))
|
||
|
||
(defvar denote-sort-dired-buffer-name-function #'denote-sort-dired-format-buffer-name
|
||
"Function to format a buffer name for `denote-sort-dired'.
|
||
It is called with all the arguments passed to `denote-sort-dired' and
|
||
must return a string that is appropriate for a buffer name.")
|
||
|
||
(defun denote-sort-dired-format-buffer-name (files-matching-regexp sort-by-component reverse exclude-regexp)
|
||
"Format buffer name for `denote-sort-dired'.
|
||
The FILES-MATCHING-REGEXP, SORT-BY-COMPONENT, REVERSE, and
|
||
EXCLUDE-REGEXP all have the same meaning as `denote-sort-dired'.
|
||
Process them to return the buffer name."
|
||
(denote-format-buffer-name
|
||
(format-message
|
||
"regexp `%s'; sort `%s'%s%s"
|
||
files-matching-regexp
|
||
sort-by-component
|
||
(if reverse "; reverse t" "")
|
||
(if exclude-regexp (format-message "; exclude-regexp `%s'" exclude-regexp) ""))
|
||
:special-buffer))
|
||
|
||
(defun denote-sort-dired--get-sort-parameters (sort-by-component reverse)
|
||
"Return (SORT-BY-COMPONENT . REVERSE) for `denote-sort-dired'.
|
||
If SORT-BY-COMPONENT is nil, use the value of the user option
|
||
`denote-sort-dired-default-sort-component' or fall back to `identifier'.
|
||
|
||
If REVERSE is nil, use the value of the user option
|
||
`denote-sort-dired-default-reverse-sort' or fall back to nil"
|
||
(cons
|
||
(or sort-by-component denote-sort-dired-default-sort-component 'identifier)
|
||
(or reverse denote-sort-dired-default-reverse-sort nil)))
|
||
|
||
(defvar denote-dired-empty-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "q") #'quit-window)
|
||
(define-key map (kbd "k") #'kill-buffer-and-window)
|
||
;; TODO 2025-07-08: Maybe this is worth implementing. The idea is
|
||
;; to go back to the search.
|
||
;;
|
||
;; (define-key map (kbd "g") #'denote-dired-empty-revert-buffer)
|
||
map)
|
||
"Keymap for `denote-dired-empty-mode'.")
|
||
|
||
(define-derived-mode denote-dired-empty-mode special-mode "Denote Dired Empty"
|
||
"Major mode of a `denote-sort-dired' that no longer matches anything."
|
||
:interactive nil
|
||
(let ((inhibit-read-only t))
|
||
(erase-buffer)
|
||
(delete-all-overlays)
|
||
(insert (propertize "Denote Dired" 'face 'bold)
|
||
"\n"
|
||
(make-separator-line)
|
||
"\n"
|
||
(propertize "No more matching files" 'face 'warning))))
|
||
|
||
(defun denote-sort-dired--get-files (regexp component reverse-sort exclude-regexp files directory)
|
||
"Do the work of `denote-sort-dired' to match and sort the FILES.
|
||
Apply REGEXP to narrow FILES and EXCLUDE-REGEXP to omit matches from the
|
||
resulting list.
|
||
|
||
If FILES is nil, search for them in the variable `denote-directory',
|
||
and then collect them relative to DIRECTORY.
|
||
|
||
At all times, sort by COMPONENT and do REVERSE-SORT if it is non-nil."
|
||
(if files
|
||
(setq files (denote-sort-files files component reverse-sort)
|
||
files (seq-filter (lambda (file) (string-match-p regexp file)) files)
|
||
files (seq-remove (lambda (file) (when exclude-regexp (string-match-p exclude-regexp file))) files))
|
||
(mapcar
|
||
(lambda (file)
|
||
(if (string-prefix-p directory file)
|
||
(substring file (length directory))
|
||
file))
|
||
(denote-sort-get-directory-files regexp component reverse-sort nil exclude-regexp))))
|
||
|
||
(defvar-local denote-sort-dired--last-arguments nil
|
||
"The last `denote-sort-dired' arguments.")
|
||
|
||
(defvar-local denote-sort-dired--last-files nil
|
||
"The last `denote-sort-dired' matched files (absolute paths).")
|
||
|
||
(defun denote-sort-dired-revert (&rest _)
|
||
"Revert the current `denote-sort-dired' buffer.
|
||
This is used as the `revert-buffer-function' for `denote-sort-dired'
|
||
buffers. It uses the values stored in the buffer-local variable
|
||
`denote-sort-dired--last-arguments'."
|
||
(pcase-let* ((`(,regexp ,component ,reverse-sort ,exclude-regexp ,files) denote-sort-dired--last-arguments))
|
||
(dlet ((ls-lisp-use-insert-directory-program (progn (require 'ls-lisp) nil)))
|
||
(if-let* ((directory (denote-directories-get-common-root))
|
||
(matched-files (denote-sort-dired--get-files regexp component reverse-sort exclude-regexp files directory)))
|
||
(progn
|
||
(setq-local dired-directory (cons directory matched-files))
|
||
(dired-revert))
|
||
(denote-dired-empty-mode)))))
|
||
|
||
;;;###autoload
|
||
(defun denote-sort-dired (regexp sort-by-component reverse exclude-regexp &optional files)
|
||
"Produce Dired buffer with sorted files from variable `denote-directory'.
|
||
When called interactively, prompt for 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. 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 REGEXP and OMIT-CURRENT have been
|
||
applied.
|
||
|
||
5. Optional FILES is a list of file paths. If it is provided,
|
||
REGEXP and EXCLUDE-REGEXP are applied to it. In
|
||
interactive use, FILES is ignored.
|
||
|
||
When called from Lisp, the mandatory arguments are (i) a string,(ii) a
|
||
symbol among `denote-sort-components', (iii) a nil or non-nil value,
|
||
and (iv) a string, respectively."
|
||
(interactive (append (list (denote-files-matching-regexp-prompt)) (denote-sort-dired--prompts)))
|
||
(pcase-let ((`(,component . ,reverse-sort) (denote-sort-dired--get-sort-parameters sort-by-component reverse)))
|
||
(dlet ((dired-buffers nil)
|
||
(ls-lisp-use-insert-directory-program (progn (require 'ls-lisp) nil)))
|
||
(if-let* ((directory (and (not (null (denote-directories)))
|
||
(denote-directories-get-common-root)))
|
||
(matched-files (denote-sort-dired--get-files regexp component reverse-sort exclude-regexp files directory))
|
||
(buffer-name (funcall denote-sort-dired-buffer-name-function regexp sort-by-component reverse-sort exclude-regexp))
|
||
(dired-buffer (dired (cons directory matched-files))))
|
||
(with-current-buffer dired-buffer
|
||
(rename-buffer buffer-name :unique)
|
||
;; NOTE 2026-04-06: I am adding the `denote-sort-dired--last-arguments' because the previous implementation
|
||
;; was not updating the existing Dired buffer after a subsequent `denote-sort-dired' call.
|
||
(let ((last-arguments (list regexp component reverse-sort exclude-regexp files)))
|
||
(cond
|
||
((null denote-sort-dired--last-arguments)
|
||
(setq-local denote-sort-dired--last-arguments last-arguments))
|
||
((not (equal denote-sort-dired--last-arguments last-arguments))
|
||
(setq-local denote-sort-dired--last-arguments last-arguments)
|
||
(denote-sort-dired-revert))))
|
||
(setq-local denote-sort-dired--last-files matched-files)
|
||
(setq-local revert-buffer-function #'denote-sort-dired-revert))
|
||
(message "No matching files for: %s" regexp)))))
|
||
|
||
(defalias 'denote-dired 'denote-sort-dired
|
||
"Alias for `denote-sort-dired' command.")
|
||
|
||
(defun denote-dired-focus (regexp &optional exclude-match)
|
||
"Filter the current `denote-dired' buffer to include only files matching REGEXP.
|
||
With optional EXCLUDE-MATCH as a prefix argument remove the files
|
||
matching REGEXP from the buffer.
|
||
|
||
This is not a new query. It builds on top of the current `denote-dired'
|
||
buffer's file list to operate only on those files."
|
||
(interactive
|
||
(if denote-sort-dired--last-files
|
||
(or (denote--user-error-if-not-major-mode 'dired-mode)
|
||
(let ((exclude-p current-prefix-arg))
|
||
(list
|
||
(denote-files-matching-regexp-prompt
|
||
(if exclude-p
|
||
"Remove files matching REGEXP in Denote Dired buffer"
|
||
"Show only files matching REGEXP in Denote Dired buffer"))
|
||
exclude-p)))
|
||
(user-error "This is not a `denote-dired' buffer")))
|
||
(denote--user-error-if-not-major-mode 'dired-mode)
|
||
(if-let* ((files denote-sort-dired--last-files))
|
||
(pcase-let* ((`(,last-regexp ,component ,reverse-sort ,exclude-regexp ,_) denote-sort-dired--last-arguments))
|
||
(if exclude-match
|
||
(denote-sort-dired last-regexp component reverse-sort regexp files)
|
||
(denote-sort-dired regexp component reverse-sort exclude-regexp files)))
|
||
(user-error "No last `denote-sort-dired' results to focus on")))
|
||
|
||
;;;; Keywords
|
||
|
||
(defun denote-extract-keywords-from-path (path)
|
||
"Extract keywords from PATH and return them as a list of strings.
|
||
PATH must be a Denote-style file name where keywords are prefixed
|
||
with an underscore.
|
||
|
||
If PATH has no such keywords, return nil.
|
||
|
||
Also see `denote-retrieve-filename-keywords'."
|
||
(when-let* ((kws (denote-retrieve-filename-keywords path)))
|
||
(split-string kws "_" :omit-nulls)))
|
||
|
||
(defalias 'denote-retrieve-filename-keywords-as-list 'denote-extract-keywords-from-path
|
||
"Alias for the function `denote-extract-keywords-from-path'")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--inferred-keywords
|
||
'denote-infer-keywords-from-files
|
||
"4.0.0")
|
||
|
||
(defun denote-infer-keywords-from-files (&optional files-matching-regexp)
|
||
"Return list of keywords in `denote-directory-files'.
|
||
With optional FILES-MATCHING-REGEXP, only extract keywords from the
|
||
matching files. Otherwise, do it for all files.
|
||
|
||
Keep any duplicates. Users who do not want duplicates should refer to
|
||
the functions `denote-keywords'."
|
||
(when-let* ((files (denote-directory-files files-matching-regexp))
|
||
(keywords (mapcan #'denote-extract-keywords-from-path files)))
|
||
(if-let* ((regexp denote-keywords-to-not-infer-regexp))
|
||
(seq-remove
|
||
(lambda (k)
|
||
(string-match-p regexp k))
|
||
keywords)
|
||
keywords)))
|
||
|
||
(defun denote-keywords (&optional files-matching-regexp)
|
||
"Return appropriate list of keyword candidates.
|
||
If `denote-infer-keywords' is non-nil, infer keywords from existing
|
||
notes and combine them into a list with `denote-known-keywords'. Else
|
||
use only the latter.
|
||
|
||
In the case of keyword inferrence, use optional FILES-MATCHING-REGEXP,
|
||
to extract keywords only from the matching files. Otherwise, do it for
|
||
all files.
|
||
|
||
Filter inferred keywords per `denote-keywords-to-not-infer-regexp'."
|
||
(delete-dups
|
||
(if denote-infer-keywords
|
||
(append (denote-infer-keywords-from-files files-matching-regexp) denote-known-keywords)
|
||
denote-known-keywords)))
|
||
|
||
(defvar denote-keyword-history nil
|
||
"Minibuffer history of inputted keywords.")
|
||
|
||
(defalias 'denote--keyword-history 'denote-keyword-history
|
||
"Compatibility alias for `denote-keyword-history'.")
|
||
|
||
(defun denote--keywords-crm (keywords &optional prompt initial)
|
||
"Use `completing-read-multiple' for KEYWORDS.
|
||
With optional PROMPT, use it instead of a generic text for file
|
||
keywords. With optional INITIAL, add it to the minibuffer as
|
||
initial input.
|
||
|
||
When `denote-infer-keywords' is nil, pass REQUIRE-MATCH to the
|
||
completion prompt. Otherwise, use nil for that parameter."
|
||
(let* ((restricted-p (null denote-infer-keywords))
|
||
(initial-prompt (or prompt "New file KEYWORDS"))
|
||
(final-prompt (if restricted-p
|
||
(format "%s %s" initial-prompt
|
||
(propertize "(controlled vocabulary)" 'face 'denote-faces-prompt-current-name))
|
||
initial-prompt)))
|
||
(delete-dups
|
||
(completing-read-multiple
|
||
(format-prompt final-prompt nil)
|
||
keywords nil restricted-p initial 'denote-keyword-history))))
|
||
|
||
(defun denote-keywords-prompt (&optional prompt-text initial-keywords infer-from-files-matching-regexp)
|
||
"Prompt for one or more keywords.
|
||
Read entries as separate when they are demarcated by the
|
||
`crm-separator', which typically is a comma.
|
||
|
||
With optional PROMPT-TEXT, use it to prompt the user for keywords. Else
|
||
use a generic prompt. With optional INITIAL-KEYWORDS use them as the
|
||
initial minibuffer text.
|
||
|
||
With optional INFER-FROM-FILES-MATCHING-REGEXP, only infer keywords from
|
||
files that match the given regular expression, per the function
|
||
`denote-keywords'.
|
||
|
||
Return an empty list if the minibuffer input is empty."
|
||
(denote--keywords-crm (denote-keywords infer-from-files-matching-regexp) prompt-text initial-keywords))
|
||
|
||
(defun denote-keywords-sort (keywords)
|
||
"Sort KEYWORDS if `denote-sort-keywords' is non-nil.
|
||
KEYWORDS is a list of strings, per `denote-keywords-prompt'."
|
||
(if denote-sort-keywords
|
||
(sort (copy-sequence keywords) #'string-collate-lessp)
|
||
keywords))
|
||
|
||
(defun denote-keywords-combine (keywords)
|
||
"Combine KEYWORDS list of strings into a single string.
|
||
Keywords are separated by the underscore character, per the
|
||
Denote file-naming scheme."
|
||
(string-join keywords "_"))
|
||
|
||
(defun denote--keywords-add-to-history (keywords)
|
||
"Append KEYWORDS to `denote-keyword-history'."
|
||
(mapc
|
||
(lambda (kw)
|
||
(add-to-history 'denote-keyword-history kw))
|
||
(delete-dups keywords)))
|
||
|
||
;;;; File types
|
||
|
||
(defvar denote-org-front-matter
|
||
"#+title: %s
|
||
#+date: %s
|
||
#+filetags: %s
|
||
#+identifier: %s
|
||
#+signature: %s
|
||
\n"
|
||
"Org front matter.
|
||
It is passed to `format' with arguments TITLE, DATE, KEYWORDS,
|
||
ID. Advanced users are advised to consult Info node `(denote)
|
||
Change the front matter format'.")
|
||
|
||
(defvar denote-yaml-front-matter
|
||
"---
|
||
title: %s
|
||
date: %s
|
||
tags: %s
|
||
identifier: %s
|
||
signature: %s
|
||
---\n\n"
|
||
"YAML (Markdown) front matter.
|
||
It is passed to `format' with arguments TITLE, DATE, KEYWORDS,
|
||
ID. Advanced users are advised to consult Info node `(denote)
|
||
Change the front matter format'.")
|
||
|
||
(defvar denote-toml-front-matter
|
||
"+++
|
||
title = %s
|
||
date = %s
|
||
tags = %s
|
||
identifier = %s
|
||
signature = %s
|
||
+++\n\n"
|
||
"TOML (Markdown) front matter.
|
||
It is passed to `format' with arguments TITLE, DATE, KEYWORDS,
|
||
ID. Advanced users are advised to consult Info node `(denote)
|
||
Change the front matter format'.")
|
||
|
||
(defvar denote-text-front-matter
|
||
"title: %s
|
||
date: %s
|
||
tags: %s
|
||
identifier: %s
|
||
signature: %s
|
||
---------------------------\n\n"
|
||
"Plain text front matter.
|
||
It is passed to `format' with arguments TITLE, DATE, KEYWORDS,
|
||
ID. Advanced users are advised to consult Info node `(denote)
|
||
Change the front matter format'.")
|
||
|
||
(defun denote-format-string-for-md-front-matter (s)
|
||
"Surround string S with quotes.
|
||
|
||
This can be used in `denote-file-types' to format front mattter."
|
||
(format "%S" s))
|
||
|
||
(defun denote-trim-whitespace (s)
|
||
"Trim whitespace around string S.
|
||
This can be used in `denote-file-types' to format front mattter."
|
||
(string-trim s))
|
||
|
||
(defun denote--trim-quotes (s)
|
||
"Trim quotes around string S."
|
||
(let ((trims "[\"']+"))
|
||
(string-trim s trims trims)))
|
||
|
||
(defun denote-trim-whitespace-then-quotes (s)
|
||
"Trim whitespace then quotes around string S.
|
||
This can be used in `denote-file-types' to format front mattter."
|
||
(denote--trim-quotes (denote-trim-whitespace s)))
|
||
|
||
(defun denote-format-string-for-org-front-matter (s)
|
||
"Return string S as-is for Org or plain text front matter."
|
||
s)
|
||
|
||
(defun denote-format-keywords-for-md-front-matter (keywords)
|
||
"Format front matter KEYWORDS for markdown file type.
|
||
KEYWORDS is a list of strings. Consult the `denote-file-types'
|
||
for how this is used."
|
||
(format "[%s]" (mapconcat (lambda (k) (format "%S" k)) keywords ", ")))
|
||
|
||
(defun denote-format-keywords-for-text-front-matter (keywords)
|
||
"Format front matter KEYWORDS for text file type.
|
||
KEYWORDS is a list of strings. Consult the `denote-file-types'
|
||
for how this is used."
|
||
(string-join keywords " "))
|
||
|
||
(defun denote-format-keywords-for-org-front-matter (keywords)
|
||
"Format front matter KEYWORDS for org file type.
|
||
KEYWORDS is a list of strings. Consult the `denote-file-types'
|
||
for how this is used."
|
||
(if keywords
|
||
(format ":%s:" (string-join keywords ":"))
|
||
""))
|
||
|
||
(defun denote-extract-keywords-from-front-matter (keywords-string)
|
||
"Extract keywords list from front matter KEYWORDS-STRING.
|
||
Split KEYWORDS-STRING into a list of strings.
|
||
|
||
Consult the `denote-file-types' for how this is used."
|
||
(split-string keywords-string "[:,\s]+" t "[][ \"']+"))
|
||
|
||
(defun denote-extract-date-from-front-matter (date-string)
|
||
"Extract date object from front matter DATE-STRING.
|
||
|
||
Consult the `denote-file-types' for how this is used."
|
||
(let ((date-string (denote-trim-whitespace date-string)))
|
||
(if (string-empty-p date-string)
|
||
nil
|
||
(date-to-time date-string))))
|
||
|
||
(defun denote-get-file-type-markdown-yaml (file)
|
||
"Return `markdown-yaml' if FILE has YAML front matter.
|
||
YAML front matter starts with --- on the first line."
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(goto-char (point-min))
|
||
(when (looking-at (format "^%s" (make-string 3 ?-)))
|
||
'markdown-yaml)))
|
||
|
||
(defun denote-get-file-type-markdown-toml (file)
|
||
"Return `markdown-toml' if FILE has TOML front matter.
|
||
TOML front matter starts with +++ on the first line."
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(goto-char (point-min))
|
||
(when (looking-at (format "^%s" (make-string 3 ?+)))
|
||
'markdown-toml)))
|
||
|
||
(defvar denote-file-types
|
||
'((org
|
||
:extension ".org"
|
||
:get-file-type-function nil
|
||
:front-matter denote-org-front-matter
|
||
:title-key-regexp "^#\\+title\\s-*:"
|
||
:title-value-function denote-format-string-for-org-front-matter
|
||
:title-value-reverse-function denote-trim-whitespace
|
||
:keywords-key-regexp "^#\\+filetags\\s-*:"
|
||
:keywords-value-function denote-format-keywords-for-org-front-matter
|
||
:keywords-value-reverse-function denote-extract-keywords-from-front-matter
|
||
:signature-key-regexp "^#\\+signature\\s-*:"
|
||
:signature-value-function denote-format-string-for-org-front-matter
|
||
:signature-value-reverse-function denote-trim-whitespace
|
||
:identifier-key-regexp "^#\\+identifier\\s-*:"
|
||
:identifier-value-function denote-format-string-for-org-front-matter
|
||
:identifier-value-reverse-function denote-trim-whitespace
|
||
:date-key-regexp "^#\\+date\\s-*:"
|
||
:date-value-function denote-date-org-timestamp
|
||
:date-value-reverse-function denote-extract-date-from-front-matter
|
||
:link-retrieval-format "[denote:%VALUE%]"
|
||
:link denote-org-link-format
|
||
:link-in-context-regexp denote-org-link-in-context-regexp)
|
||
(markdown-yaml
|
||
:extension ".md"
|
||
:get-file-type-function denote-get-file-type-markdown-yaml
|
||
:front-matter denote-yaml-front-matter
|
||
:title-key-regexp "^title\\s-*:"
|
||
:title-value-function denote-format-string-for-md-front-matter
|
||
:title-value-reverse-function denote-trim-whitespace-then-quotes
|
||
:keywords-key-regexp "^tags\\s-*:"
|
||
:keywords-value-function denote-format-keywords-for-md-front-matter
|
||
:keywords-value-reverse-function denote-extract-keywords-from-front-matter
|
||
:signature-key-regexp "^signature\\s-*:"
|
||
:signature-value-function denote-format-string-for-md-front-matter
|
||
:signature-value-reverse-function denote-trim-whitespace-then-quotes
|
||
:identifier-key-regexp "^identifier\\s-*:"
|
||
:identifier-value-function denote-format-string-for-md-front-matter
|
||
:identifier-value-reverse-function denote-trim-whitespace-then-quotes
|
||
:date-key-regexp "^date\\s-*:"
|
||
:date-value-function denote-date-rfc3339
|
||
:date-value-reverse-function denote-extract-date-from-front-matter
|
||
:link-retrieval-format "(denote:%VALUE%)"
|
||
:link denote-md-link-format
|
||
:link-in-context-regexp denote-md-link-in-context-regexp)
|
||
(markdown-toml
|
||
:extension ".md"
|
||
:get-file-type-function denote-get-file-type-markdown-toml
|
||
:front-matter denote-toml-front-matter
|
||
:title-key-regexp "^title\\s-*="
|
||
:title-value-function denote-format-string-for-md-front-matter
|
||
:title-value-reverse-function denote-trim-whitespace-then-quotes
|
||
:keywords-key-regexp "^tags\\s-*="
|
||
:keywords-value-function denote-format-keywords-for-md-front-matter
|
||
:keywords-value-reverse-function denote-extract-keywords-from-front-matter
|
||
:signature-key-regexp "^signature\\s-*="
|
||
:signature-value-function denote-format-string-for-md-front-matter
|
||
:signature-value-reverse-function denote-trim-whitespace-then-quotes
|
||
:identifier-key-regexp "^identifier\\s-*="
|
||
:identifier-value-function denote-format-string-for-md-front-matter
|
||
:identifier-value-reverse-function denote-trim-whitespace-then-quotes
|
||
:date-key-regexp "^date\\s-*="
|
||
:date-value-function denote-date-rfc3339
|
||
:date-value-reverse-function denote-extract-date-from-front-matter
|
||
:link-retrieval-format "(denote:%VALUE%)"
|
||
:link denote-md-link-format
|
||
:link-in-context-regexp denote-md-link-in-context-regexp)
|
||
(text
|
||
:extension ".txt"
|
||
:get-file-type-function nil
|
||
:front-matter denote-text-front-matter
|
||
:title-key-regexp "^title\\s-*:"
|
||
:title-value-function denote-format-string-for-org-front-matter
|
||
:title-value-reverse-function denote-trim-whitespace
|
||
:keywords-key-regexp "^tags\\s-*:"
|
||
:keywords-value-function denote-format-keywords-for-text-front-matter
|
||
:keywords-value-reverse-function denote-extract-keywords-from-front-matter
|
||
:signature-key-regexp "^signature\\s-*:"
|
||
:signature-value-function denote-format-string-for-org-front-matter
|
||
:signature-value-reverse-function denote-trim-whitespace
|
||
:identifier-key-regexp "^identifier\\s-*:"
|
||
:identifier-value-function denote-format-string-for-org-front-matter
|
||
:identifier-value-reverse-function denote-trim-whitespace
|
||
:date-key-regexp "^date\\s-*:"
|
||
:date-value-function denote-date-iso-8601
|
||
:date-value-reverse-function denote-extract-date-from-front-matter
|
||
:link-retrieval-format "[denote:%VALUE%]"
|
||
:link denote-org-link-format
|
||
:link-in-context-regexp denote-org-link-in-context-regexp))
|
||
"Alist of variable `denote-file-type' and their format properties.
|
||
|
||
Each element is of the form (SYMBOL PROPERTY-LIST). SYMBOL is one of
|
||
those specified in the user option `denote-file-type' or an arbitrary
|
||
symbol that defines a new file type.
|
||
|
||
PROPERTY-LIST is a plist that consists of the following elements:
|
||
|
||
- `:extension' is a string with the file extension including the
|
||
period.
|
||
|
||
- `:get-file-type-function' a function with one parameter, a given file,
|
||
that reads the file contents and returns the file type as a symbol or
|
||
nil. If there is no function, Denote will fall back to a search for a
|
||
title in the front matter (per `:title-key-regexp', which is described
|
||
below).
|
||
|
||
- `:date-function' is a function that can format a date. See the
|
||
functions `denote-date-iso-8601', `denote-date-rfc3339', and
|
||
`denote-date-org-timestamp'.
|
||
|
||
- `:front-matter' is either a string passed to `format' or a
|
||
variable holding such a string. The `format' function accepts
|
||
four arguments, which come from `denote' in this order: TITLE,
|
||
DATE, KEYWORDS, IDENTIFIER. Read the docstring of `format' on
|
||
how to reorder arguments.
|
||
|
||
- `:title-key-regexp' is a regular expression that is used to
|
||
retrieve the title line in a file. The first line matching
|
||
this regexp is considered the title line.
|
||
|
||
- `:title-value-function' is the function used to format the raw
|
||
title string for inclusion in the front matter (e.g. to
|
||
surround it with quotes). Use the `identity' function if no
|
||
further processing is required.
|
||
|
||
- `:title-value-reverse-function' is the function used to
|
||
retrieve the raw title string from the front matter. It
|
||
performs the reverse of `:title-value-function'.
|
||
|
||
- `:keywords-key-regexp' is a regular expression used to retrieve
|
||
the keywords' line in the file. The first line matching this
|
||
regexp is considered the keywords' line.
|
||
|
||
- `:keywords-value-function' is the function used to format the
|
||
keywords' list of strings as a single string, with appropriate
|
||
delimiters, for inclusion in the front matter.
|
||
|
||
- `:keywords-value-reverse-function' is the function used to
|
||
retrieve the keywords' value from the front matter. It
|
||
performs the reverse of the `:keywords-value-function'.
|
||
|
||
- `:link-retrieval-format' is a string, or variable holding a string,
|
||
that specifies the retrieval format of a link.
|
||
|
||
- `:link' is a string, or variable holding a string, that
|
||
specifies the format of a link. See the variables
|
||
`denote-org-link-format', `denote-md-link-format'.
|
||
|
||
- `:link-in-context-regexp' is a regular expression that is used
|
||
to match the aforementioned link format. See the variables
|
||
`denote-org-link-in-context-regexp',`denote-md-link-in-context-regexp'.
|
||
|
||
If the user option `denote-file-type' is nil, use the first element of
|
||
this list for new note creation. The default is `org'.")
|
||
|
||
(defun denote--file-extension (file-type)
|
||
"Return file type extension based on FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:extension))
|
||
|
||
(defun denote--front-matter (file-type)
|
||
"Return front matter based on FILE-TYPE."
|
||
(let ((prop (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:front-matter)))
|
||
(if (symbolp prop)
|
||
(symbol-value prop)
|
||
prop)))
|
||
|
||
(defun denote--title-key-regexp (file-type)
|
||
"Return the title key regexp associated to FILE-TYPE."
|
||
(or (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:title-key-regexp)
|
||
"^DenoteUserWantsEmptyFieldSoHandleIt")) ; Will not be found
|
||
|
||
(defun denote--title-value-function (file-type)
|
||
"Convert title string to a front matter title, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:title-value-function))
|
||
|
||
;; NOTE 2026-04-23: This is used in the `denote--define-retrieve-front-matter-from-content'.
|
||
;; I could not find it with a search, which made me think it was not being used.
|
||
(defun denote--title-value-reverse-function (file-type)
|
||
"Convert front matter title to the title string, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:title-value-reverse-function))
|
||
|
||
(defun denote--keywords-key-regexp (file-type)
|
||
"Return the keywords key regexp associated to FILE-TYPE."
|
||
(or (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:keywords-key-regexp)
|
||
"^DenoteUserWantsEmptyFieldSoHandleIt")) ; Will not be found
|
||
|
||
(defun denote--keywords-value-function (file-type)
|
||
"Convert keywords' list to front matter keywords, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:keywords-value-function))
|
||
|
||
(defun denote--keywords-value-reverse-function (file-type)
|
||
"Convert front matter keywords to keywords' list, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:keywords-value-reverse-function))
|
||
|
||
(defun denote--signature-key-regexp (file-type)
|
||
"Return the signature key regexp associated to FILE-TYPE."
|
||
(or (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:signature-key-regexp)
|
||
"^DenoteUserWantsEmptyFieldSoHandleIt")) ; Will not be found
|
||
|
||
(defun denote--signature-value-function (file-type)
|
||
"Convert signature string to front matter signature, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:signature-value-function))
|
||
|
||
(defun denote--signature-value-reverse-function (file-type)
|
||
"Convert front matter signature to signature string, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:signature-value-reverse-function))
|
||
|
||
(defun denote--identifier-key-regexp (file-type)
|
||
"Return the identifier key regexp associated to FILE-TYPE."
|
||
(or (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:identifier-key-regexp)
|
||
"^DenoteUserWantsEmptyFieldSoHandleIt")) ; Will not be found
|
||
|
||
(defun denote--identifier-value-function (file-type)
|
||
"Convert identifier string to front matter identifier, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:identifier-value-function))
|
||
|
||
(defun denote--identifier-value-reverse-function (file-type)
|
||
"Convert front matter identifier to identifier string, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:identifier-value-reverse-function))
|
||
|
||
(defun denote--date-key-regexp (file-type)
|
||
"Return the date key regexp associated to FILE-TYPE."
|
||
(or (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:date-key-regexp)
|
||
"^DenoteUserWantsEmptyFieldSoHandleIt")) ; Will not be found
|
||
|
||
(defun denote--date-value-function (file-type)
|
||
"Convert date object to front matter date, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:date-value-function))
|
||
|
||
(defun denote--date-value-reverse-function (file-type)
|
||
"Convert front matter date to date object, per FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:date-value-reverse-function))
|
||
|
||
(defun denote--link-retrieval-format (file-type)
|
||
"Return link retrieval format based on FILE-TYPE."
|
||
(let ((prop (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:link-retrieval-format)))
|
||
(if (symbolp prop)
|
||
(symbol-value prop)
|
||
prop)))
|
||
|
||
(defun denote--link-format (file-type)
|
||
"Return link format extension based on FILE-TYPE."
|
||
(let ((prop (plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:link)))
|
||
(if (symbolp prop)
|
||
(symbol-value prop)
|
||
prop)))
|
||
|
||
(defvar denote-id-only-link-in-context-regexp)
|
||
|
||
(defun denote--link-in-context-regexp (file-type)
|
||
"Return link regexp in context based on FILE-TYPE."
|
||
(when-let* ((type (alist-get file-type denote-file-types))
|
||
(property (plist-get type :link-in-context-regexp))
|
||
(link-type-regexp (if (symbolp property)
|
||
(symbol-value property)
|
||
property)))
|
||
(format "%s\\|%s" link-type-regexp denote-id-only-link-in-context-regexp)))
|
||
|
||
(defun denote-file-type-extensions ()
|
||
"Return all file type extensions in `denote-file-types'."
|
||
(delete-dups
|
||
(mapcar (lambda (type)
|
||
(plist-get (cdr type) :extension))
|
||
denote-file-types)))
|
||
|
||
(defun denote--file-type-keys ()
|
||
"Return all `denote-file-types' keys."
|
||
(delete-dups (mapcar #'car denote-file-types)))
|
||
|
||
(defun denote--get-component-key-regexp-function (component)
|
||
"Return COMPONENT's key regexp function.
|
||
|
||
COMPONENT can be one of `title', `keywords', `identifier', `date', `signature'."
|
||
(pcase component
|
||
('title #'denote--title-key-regexp)
|
||
('keywords #'denote--keywords-key-regexp)
|
||
('signature #'denote--signature-key-regexp)
|
||
('date #'denote--date-key-regexp)
|
||
('identifier #'denote--identifier-key-regexp)))
|
||
|
||
(defun denote--format-front-matter (title date keywords identifier signature filetype)
|
||
"Front matter for new notes.
|
||
|
||
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))
|
||
(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)) ""))
|
||
(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 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 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))
|
||
(not (memq component denote-front-matter-components-present-even-if-empty-value))
|
||
(re-search-forward (funcall component-key-regexp-function filetype) nil t 1))
|
||
(goto-char (line-beginning-position))
|
||
(delete-region (line-beginning-position) (line-beginning-position 2)))))
|
||
(buffer-string))))
|
||
|
||
;;;; Front matter or content retrieval functions
|
||
|
||
(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 referred by
|
||
`denote-get-identifier-function'."
|
||
(let ((filename (file-name-nondirectory file)))
|
||
(cond ((string-match (concat "\\`" denote-date-identifier-regexp) filename)
|
||
(match-string-no-properties 0 filename))
|
||
((string-match denote-identifier-regexp filename)
|
||
(match-string-no-properties 1 filename)))))
|
||
|
||
;; TODO 2023-12-08: Maybe we can only use
|
||
;; `denote-retrieve-filename-identifier' and remove this function.
|
||
(defun denote-retrieve-filename-identifier-with-error (file)
|
||
"Extract identifier from FILE name, if present, else signal an error."
|
||
(or (denote-retrieve-filename-identifier file)
|
||
(error "Cannot find `%s' as a file with a Denote identifier" file)))
|
||
|
||
(make-obsolete
|
||
'denote-retrieve-filename-identifier-with-error
|
||
'denote-retrieve-filename-identifier
|
||
"4.1.0")
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote--used-ids
|
||
'denote-used-identifiers
|
||
"4.1.0")
|
||
|
||
(defvar denote-used-identifiers nil
|
||
"Hash table of used identifiers.
|
||
This variable should be set only for the duration of a command.
|
||
It should stay nil otherwise.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-create-unique-file-identifier
|
||
'denote-get-identifier
|
||
"4.0.0")
|
||
|
||
(make-obsolete 'denote-get-identifier 'denote-get-identifier-function "4.1.0")
|
||
|
||
(defun denote-retrieve-filename-keywords (file)
|
||
"Extract keywords from FILE name, if present, else return nil.
|
||
Return matched keywords as a single string.
|
||
|
||
Also see `denote-extract-keywords-from-path' (alias
|
||
`denote-retrieve-filename-keywords-as-list')."
|
||
(let ((filename (file-name-nondirectory file)))
|
||
(when (string-match denote-keywords-regexp filename)
|
||
(match-string 1 filename))))
|
||
|
||
(defun denote-retrieve-filename-signature (file)
|
||
"Extract signature from FILE name, if present, else return nil."
|
||
(let ((filename (file-name-nondirectory file)))
|
||
(when (string-match denote-signature-regexp filename)
|
||
(match-string 1 filename))))
|
||
|
||
(defun denote-retrieve-filename-title (file)
|
||
"Extract Denote title component from FILE name, else return nil."
|
||
(let ((filename (file-name-nondirectory file)))
|
||
(when (string-match denote-title-regexp filename)
|
||
(match-string 1 filename))))
|
||
|
||
(defmacro denote--file-with-temp-buffer (file &rest body)
|
||
"Evaluate BODY with FILE contents in scope.
|
||
If a buffer is visiting FILE, insert the buffer contents into a
|
||
temporary buffer. Otherwise, insert the contents of FILE. Then call
|
||
BODY."
|
||
(declare (indent 1))
|
||
`(let* ((buffer (get-file-buffer ,file))
|
||
(file-exist (file-exists-p ,file)))
|
||
(with-temp-buffer
|
||
(cond
|
||
(buffer (insert-buffer-substring buffer))
|
||
(file-exist (insert-file-contents ,file))
|
||
(t (error "Cannot find anything about file `%s'" ,file)))
|
||
(goto-char (point-min))
|
||
,@body)))
|
||
|
||
(defmacro denote--define-retrieve-front-matter (component scope)
|
||
"Define a function to retrieve front matter for COMPONENT given SCOPE.
|
||
The COMPONENT is one of the file name components that has a
|
||
corresponding front matter entry. SCOPE is a symbol of either `value'
|
||
or `line', referring to what the function should retrieve."
|
||
(declare (indent 1))
|
||
`(defun ,(intern (format "denote-retrieve-front-matter-%s-%s" component scope)) (file file-type)
|
||
(when file-type
|
||
(denote--file-with-temp-buffer file
|
||
(when (re-search-forward (,(intern (format "denote--%s-key-regexp" component)) file-type) nil t 1)
|
||
,(cond
|
||
((eq scope 'value)
|
||
`(funcall (,(intern (format "denote--%s-value-reverse-function" component)) file-type)
|
||
(buffer-substring-no-properties (point) (line-end-position))))
|
||
((eq scope 'line)
|
||
'(buffer-substring-no-properties (line-beginning-position) (line-end-position)))
|
||
(t (error "`%s' is not a known scope" scope))))))))
|
||
|
||
(denote--define-retrieve-front-matter title value)
|
||
(denote--define-retrieve-front-matter title line)
|
||
(denote--define-retrieve-front-matter keywords value)
|
||
(denote--define-retrieve-front-matter keywords line)
|
||
(denote--define-retrieve-front-matter signature value)
|
||
(denote--define-retrieve-front-matter signature line)
|
||
(denote--define-retrieve-front-matter identifier value)
|
||
(denote--define-retrieve-front-matter identifier line)
|
||
(denote--define-retrieve-front-matter date value)
|
||
(denote--define-retrieve-front-matter date line)
|
||
|
||
(defun denote--regexp-in-file-p (regexp file)
|
||
"Return t if REGEXP matches in the FILE."
|
||
(denote--file-with-temp-buffer file
|
||
(re-search-forward regexp nil t 1)))
|
||
|
||
(defun denote--get-front-matter-value-no-indentation (component line file-type)
|
||
"Return the COMPONENT on LINE given the FILE-TYPE without any indentation."
|
||
(when-let* ((key-regexp-fn (denote--get-component-key-regexp-function component))
|
||
(regexp (funcall key-regexp-fn file-type)))
|
||
(with-temp-buffer
|
||
(insert line)
|
||
(goto-char (point-min))
|
||
(when (re-search-forward regexp nil t 1)
|
||
(re-search-forward "\\s-*" (line-end-position) t)
|
||
(buffer-substring-no-properties (point) (line-end-position))))))
|
||
|
||
(defun denote--rewrite-front-matter-line (component new-line file-type)
|
||
"Rewrite COMPONENT with NEW-LINE given the FILE-TYPE.
|
||
Preserve the existing key and the spacing, if any, that follows it.
|
||
Return non-nil if successful."
|
||
(when-let* ((key-regexp-fn (denote--get-component-key-regexp-function component))
|
||
(regexp (funcall key-regexp-fn file-type))
|
||
(_ (re-search-forward regexp nil t 1))
|
||
(new-value (denote--get-front-matter-value-no-indentation component new-line file-type)))
|
||
(re-search-forward "\\s-*" (line-end-position) t)
|
||
(delete-region (point) (line-end-position))
|
||
(insert new-value)
|
||
t))
|
||
|
||
;; These are private front matter retrieval functions, working with a content parameter
|
||
|
||
(defmacro denote--define-retrieve-front-matter-from-content (component scope)
|
||
"Define a function to retrieve front matter for COMPONENT given SCOPE.
|
||
The COMPONENT is one of the file name components that has a
|
||
corresponding front matter entry. SCOPE is a symbol of either `value'
|
||
or `line', referring to what the function should retrieve."
|
||
(declare (indent 1))
|
||
`(defun ,(intern (format "denote--retrieve-front-matter-%s-%s-from-content" component scope)) (content file-type)
|
||
(when file-type
|
||
(with-temp-buffer
|
||
(insert content)
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (,(intern (format "denote--%s-key-regexp" component)) file-type) nil t 1)
|
||
,(cond
|
||
((eq scope 'value)
|
||
`(funcall (,(intern (format "denote--%s-value-reverse-function" component)) file-type)
|
||
(buffer-substring-no-properties (point) (line-end-position))))
|
||
((eq scope 'line)
|
||
'(buffer-substring-no-properties (line-beginning-position) (line-end-position)))
|
||
(t (error "`%s' is not a known scope" scope))))))))
|
||
|
||
(denote--define-retrieve-front-matter-from-content title value)
|
||
(denote--define-retrieve-front-matter-from-content title line)
|
||
(denote--define-retrieve-front-matter-from-content keywords value)
|
||
(denote--define-retrieve-front-matter-from-content keywords line)
|
||
(denote--define-retrieve-front-matter-from-content signature value)
|
||
(denote--define-retrieve-front-matter-from-content signature line)
|
||
(denote--define-retrieve-front-matter-from-content identifier value)
|
||
(denote--define-retrieve-front-matter-from-content identifier line)
|
||
(denote--define-retrieve-front-matter-from-content date value)
|
||
(denote--define-retrieve-front-matter-from-content date line)
|
||
|
||
(defalias 'denote-retrieve-title-value 'denote-retrieve-front-matter-title-value
|
||
"Alias for `denote-retrieve-front-matter-title-value'.")
|
||
|
||
(defalias 'denote-retrieve-title-line 'denote-retrieve-front-matter-title-line
|
||
"Alias for `denote-retrieve-front-matter-title-line'.")
|
||
|
||
(defalias 'denote-retrieve-keywords-value 'denote-retrieve-front-matter-keywords-value
|
||
"Alias for `denote-retrieve-front-matter-keywords-value'.")
|
||
|
||
(defalias 'denote-retrieve-keywords-line 'denote-retrieve-front-matter-keywords-line
|
||
"Alias for `denote-retrieve-front-matter-keywords-line'.")
|
||
|
||
(defun denote-retrieve-title-or-filename (file type)
|
||
"Return appropriate title for FILE given its TYPE.
|
||
This is a wrapper for `denote-retrieve-front-matter-title-value' and
|
||
`denote-retrieve-filename-title'."
|
||
(let ((has-denoted-filename (denote-file-has-denoted-filename-p file))
|
||
(has-supported-extension (denote-file-has-supported-extension-p file)))
|
||
(cond ((and has-denoted-filename has-supported-extension)
|
||
(or (denote-retrieve-front-matter-title-value file type)
|
||
(denote-retrieve-filename-title file)
|
||
""))
|
||
(has-denoted-filename
|
||
(or (denote-retrieve-filename-title file) ""))
|
||
(t
|
||
(file-name-base file)))))
|
||
|
||
(make-obsolete 'denote--retrieve-location-in-xrefs 'denote-retrieve-groups-xref-query "4.0.0")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--retrieve-group-in-xrefs
|
||
'denote-retrieve-groups-xref-query
|
||
"4.0.0")
|
||
|
||
(defun denote-retrieve-groups-xref-query (query &optional files-matching-regexp)
|
||
"Access location of xrefs for QUERY and group them per file.
|
||
Limit the search to text files. With optional FILES-MATCHING-REGEXP,
|
||
pass it to `denote-directory-files'."
|
||
(when-let* ((files (denote-directory-files files-matching-regexp nil :text-only))
|
||
(locations (mapcar #'xref-match-item-location (xref-matches-in-files query files))))
|
||
(mapcar #'xref-location-group locations)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--retrieve-files-in-xrefs
|
||
'denote-retrieve-files-xref-query
|
||
"4.0.0")
|
||
|
||
(defun denote-retrieve-files-xref-query (query &optional files-matching-regexp)
|
||
"Return sorted, deduplicated file names with matches for QUERY in their contents.
|
||
Limit the search to text files. With optional FILES-MATCHING-REGEXP,
|
||
pass it to `denote-directory-files'."
|
||
(sort
|
||
(delete-dups
|
||
(denote-retrieve-groups-xref-query query files-matching-regexp))
|
||
#'string-collate-lessp))
|
||
|
||
(defvar denote-query--last-files nil
|
||
"List of files matched by the last call to `denote-make-links-buffer'.")
|
||
|
||
(defvar denote-query--last-query nil
|
||
"String of the last call to `denote-make-links-buffer'.")
|
||
|
||
(defvar denote-query--omit-current t
|
||
"When non-nil `denote-make-links-buffer' omits the current file.")
|
||
|
||
(defvar denote-query-sorting)
|
||
|
||
(defun denote-retrieve-xref-alist (query &optional files)
|
||
"Return xref alist of absolute file paths with location of matches for QUERY.
|
||
Optional FILES can be a list of files to search for. It can also be a
|
||
regular expression, which means to use the text files in the variable
|
||
`denote-directory' that match that regexp.
|
||
|
||
If FILES is not given, use all text files as returned by
|
||
`denote-directory-files'."
|
||
(let* ((xref-file-name-display 'abs)
|
||
(data
|
||
(xref--analyze
|
||
(xref-matches-in-files
|
||
query
|
||
(if (and files (listp files))
|
||
files
|
||
(denote-directory-files files denote-query--omit-current :text-only))))))
|
||
(if-let* ((sort denote-query-sorting)
|
||
(files-matched (mapcar #'car data))
|
||
(files-sorted (denote-sort-files files-matched sort)))
|
||
(mapcar (lambda (x) (assoc x data)) files-sorted)
|
||
data)))
|
||
|
||
(defun denote--get-files-by-file-type (files)
|
||
"Return hash table of FILES by file type."
|
||
(let ((file-type-hash-table (make-hash-table))
|
||
(file-types (denote--file-type-keys)))
|
||
(dolist (file-type file-types)
|
||
(puthash file-type '() file-type-hash-table))
|
||
(dolist (file files)
|
||
(when-let* ((file-type (denote-file-type file)))
|
||
(push file (gethash file-type file-type-hash-table))))
|
||
file-type-hash-table))
|
||
|
||
(defun denote--get-all-backlinks (files)
|
||
"Return hash table of all backlinks in FILES by identifier."
|
||
(let ((links-hash-table (make-hash-table :test #'equal))
|
||
(file-types (denote--file-type-keys))
|
||
(files-by-file-type (denote--get-files-by-file-type files)))
|
||
(dolist (file-type file-types)
|
||
(let* ((file-type-files (gethash file-type files-by-file-type))
|
||
(regexp (denote--link-in-context-regexp file-type)))
|
||
(dolist (file file-type-files)
|
||
(let* ((file-identifiers
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(denote-link--collect-identifiers regexp))))
|
||
(dolist (file-identifier file-identifiers)
|
||
(if-let* ((links (gethash file-identifier links-hash-table)))
|
||
(puthash file-identifier (push file links) links-hash-table)
|
||
(puthash file-identifier (list file) links-hash-table)))))))
|
||
links-hash-table))
|
||
|
||
(defun denote-retrieve-xref-alist-for-backlinks (identifier)
|
||
"Return xref alist of absolute file paths of matches for IDENTIFIER."
|
||
(let* ((files (denote-directory-files))
|
||
(file-types (denote--file-type-keys))
|
||
(xref-file-name-display 'abs)
|
||
(xref-matches '()))
|
||
(when-let* ((backlinks (gethash identifier (denote--get-all-backlinks files))))
|
||
(let* ((backlinks-by-file-type (denote--get-files-by-file-type backlinks)))
|
||
(dolist (file-type file-types)
|
||
(when-let* ((current-backlinks (gethash file-type backlinks-by-file-type))
|
||
(type (denote--link-retrieval-format file-type))
|
||
(format-parts (split-string type "%VALUE%")) ; Should give two parts
|
||
(query-simple (concat
|
||
(regexp-quote (nth 0 format-parts))
|
||
(regexp-quote identifier)
|
||
(regexp-quote (nth 1 format-parts))))
|
||
(query-org-link (concat
|
||
(regexp-quote (nth 0 format-parts))
|
||
(regexp-quote identifier)
|
||
"::")))
|
||
(setq xref-matches (append xref-matches (xref-matches-in-files query-simple current-backlinks)))
|
||
(setq xref-matches (append xref-matches (xref-matches-in-files query-org-link current-backlinks))))))
|
||
(let ((data (xref--analyze xref-matches)))
|
||
(if-let* ((sort denote-query-sorting)
|
||
(files-matched (mapcar #'car data))
|
||
(files-sorted (denote-sort-files files-matched sort)))
|
||
(mapcar (lambda (x) (assoc x data)) files-sorted)
|
||
data)))))
|
||
|
||
;;;; New note
|
||
|
||
;;;;; Common helpers for new notes
|
||
|
||
(defun denote-format-file-name (dir-path id keywords title extension signature)
|
||
"Format file name.
|
||
DIR-PATH, ID, KEYWORDS, TITLE, EXTENSION and SIGNATURE are
|
||
expected to be supplied by `denote' or equivalent command.
|
||
|
||
DIR-PATH is a string pointing to a directory. It ends with a
|
||
forward slash (the function `denote-directories' makes sure this is
|
||
the case when returning the value of the variable `denote-directory').
|
||
DIR-PATH cannot be nil or an empty string.
|
||
|
||
ID is a string holding the identifier of the note. It can be an
|
||
empty string, in which case its respective file name component is
|
||
not added to the base file name.
|
||
|
||
DIR-PATH and ID form the base file name.
|
||
|
||
KEYWORDS is a list of strings that is reduced to a single string
|
||
by `denote-keywords-combine'. KEYWORDS can be an empty list or
|
||
a nil value, in which case the relevant file name component is
|
||
not added to the base file name.
|
||
|
||
TITLE and SIGNATURE are strings. They can be an empty string, in
|
||
which case their respective file name component is not added to
|
||
the base file name.
|
||
|
||
EXTENSION is a string that contains a dot followed by the file
|
||
type extension. It can be an empty string or a nil value, in
|
||
which case it is not added to the base file name."
|
||
(cond
|
||
((null dir-path)
|
||
(error "DIR-PATH must not be nil"))
|
||
((string-empty-p dir-path)
|
||
(error "DIR-PATH must not be an empty string"))
|
||
((not (string-suffix-p "/" dir-path))
|
||
(error "DIR-PATH does not end with a / as directories ought to")))
|
||
(let ((file-name "")
|
||
(components (seq-union denote-file-name-components-order
|
||
'(identifier signature title keywords))))
|
||
(dolist (component components)
|
||
(cond ((and (eq component 'identifier) id (not (string-empty-p 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)
|
||
(setq file-name (concat file-name "__" (denote-keywords-combine (denote-sluggify-keywords-and-apply-rules keywords)))))
|
||
((and (eq component 'signature) signature (not (string-empty-p signature)))
|
||
(setq file-name (concat file-name "==" (denote-sluggify-and-apply-rules 'signature signature))))))
|
||
(when (string-empty-p file-name)
|
||
(error "There should be at least one file name component"))
|
||
(setq file-name (concat file-name extension))
|
||
;; Do not prepend identifier with @@ if it is the first component and has the format 00000000T000000.
|
||
(when (and (not denote-identifier-delimiter-always-present-in-file-name)
|
||
(string-prefix-p "@@" file-name)
|
||
(string-match-p (concat "\\`" denote-date-identifier-regexp "\\'") id))
|
||
(setq file-name (substring file-name 2)))
|
||
(concat dir-path file-name)))
|
||
|
||
;; Adapted from `org-hugo--org-date-time-to-rfc3339' in the `ox-hugo'
|
||
;; package: <https://github.com/kaushalmodi/ox-hugo>.
|
||
(defun denote-date-rfc3339 (date)
|
||
"Format DATE using the RFC3339 specification."
|
||
(if date
|
||
(replace-regexp-in-string
|
||
"\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)\\'" "\\1:\\2"
|
||
(format-time-string "%FT%T%z" date))
|
||
""))
|
||
|
||
(defun denote-date-org-timestamp (date)
|
||
"Format DATE using the Org inactive timestamp notation."
|
||
(if date
|
||
(format-time-string "[%F %a %R]" date)
|
||
""))
|
||
|
||
(defun denote-date-iso-8601 (date)
|
||
"Format DATE according to ISO 8601 standard."
|
||
(if date
|
||
(format-time-string "%F" date)
|
||
""))
|
||
|
||
(defun denote--format-front-matter-date (date file-type)
|
||
"Expand DATE in an appropriate format for FILE-TYPE."
|
||
(let ((format denote-date-format))
|
||
(cond
|
||
(format
|
||
(if date (format-time-string format date) ""))
|
||
((when-let* ((fn (denote--date-value-function file-type)))
|
||
(funcall fn date)))
|
||
(t
|
||
(denote-date-org-timestamp date)))))
|
||
|
||
(defun denote--prepare-note (title keywords date id directory file-type template signature)
|
||
"Prepare a new note file and return its path.
|
||
|
||
Arguments TITLE, KEYWORDS, DATE, ID, DIRECTORY, FILE-TYPE,
|
||
TEMPLATE, and SIGNATURE should be valid for note creation."
|
||
(let* ((extension (denote--file-extension file-type))
|
||
(path (denote-format-file-name directory id keywords title extension signature))
|
||
;; TODO 2025-08-02: Is it safe to assume that an existing and
|
||
;; empty file is good for us to use? Otherwise, we should
|
||
;; have a `y-or-n-p' prompt here.
|
||
(buffer (if (and (file-regular-p path) (not (denote--file-empty-p path)))
|
||
(user-error "A file named `%s' already exists and is not empty" path)
|
||
(find-file path)))
|
||
(header (denote--format-front-matter title date keywords id signature file-type)))
|
||
(with-current-buffer buffer
|
||
(insert header)
|
||
(insert (cond
|
||
((stringp template) template)
|
||
((functionp template) (funcall template))
|
||
(t (user-error "Invalid template")))))
|
||
path))
|
||
|
||
(defun denote--dir-in-denote-directory-p (directory)
|
||
"Return non-nil if DIRECTORY is in variable `denote-directory'."
|
||
(seq-some
|
||
(lambda (d)
|
||
(string-prefix-p d (file-name-as-directory (expand-file-name directory))))
|
||
(denote-directories)))
|
||
|
||
(defun denote--valid-file-type (filetype)
|
||
"Return a valid filetype symbol given the argument FILETYPE.
|
||
If none is found, the first element of `denote-file-types' is
|
||
returned."
|
||
(let ((type (cond
|
||
((stringp filetype) (intern filetype))
|
||
((symbolp filetype) filetype)
|
||
(t (error "The `%s' is neither a string nor a symbol" filetype)))))
|
||
(cond
|
||
((memq type (denote--file-type-keys))
|
||
type)
|
||
((null denote-file-types)
|
||
(user-error "At least one file type must be defined in `denote-file-types' to create a note"))
|
||
(t
|
||
(caar denote-file-types)))))
|
||
|
||
(defun denote--date-add-current-time (date)
|
||
"Add current time to DATE, if necessary.
|
||
The idea is to turn 2020-01-15 into 2020-01-15 16:19 so that the
|
||
hour and minute component is not left to 00:00.
|
||
|
||
This reduces the burden on the user who would otherwise need to
|
||
input that value in order to avoid the error of duplicate
|
||
identifiers.
|
||
|
||
It also addresses a difference between Emacs 28 and Emacs 29
|
||
where the former does not read dates without a time component."
|
||
(if (<= (length date) 10)
|
||
(format "%s %s" date (format-time-string "%H:%M:%S" (current-time)))
|
||
date))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-parse-date
|
||
'denote-valid-date-p
|
||
"4.0.0")
|
||
|
||
(defun denote-valid-date-p (date)
|
||
"Return DATE as a valid date.
|
||
A valid DATE is a value that can be parsed by either
|
||
`decode-time' or `date-to-time'. Those functions signal an error
|
||
if DATE is a value they do not recognise.
|
||
|
||
If DATE is nil or an empty string, return nil."
|
||
(cond
|
||
((null date)
|
||
nil)
|
||
((and (stringp date) (string-empty-p date))
|
||
nil)
|
||
((and (or (numberp date) (listp date))
|
||
(decode-time date))
|
||
date)
|
||
(t ; non-empty strings (e.g. "2024-01-01", "2024-01-01 12:00", etc.)
|
||
(date-to-time (denote--date-add-current-time date)))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--id-to-date
|
||
'denote-id-to-date
|
||
"4.1.0")
|
||
|
||
(defun denote-id-to-date (identifier)
|
||
"Convert IDENTIFIER string to YYYY-MM-DD."
|
||
(if (denote-date-identifier-p identifier)
|
||
(replace-regexp-in-string
|
||
"\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\).*"
|
||
"\\1-\\2-\\3"
|
||
identifier)
|
||
(error "`%s' does not look like a Denote identifier per `denote-date-identifier-regexp'" identifier)))
|
||
|
||
(defun denote--buffer-file-names ()
|
||
"Return file names of Denote buffers."
|
||
(delq nil
|
||
(mapcar
|
||
(lambda (buffer)
|
||
(when-let* (((buffer-live-p buffer))
|
||
(file (buffer-file-name buffer))
|
||
((denote-file-is-in-denote-directory-p file))
|
||
((denote-file-has-supported-extension-p file))
|
||
((denote-file-has-denoted-filename-p file))
|
||
((denote-file-has-identifier-p file)))
|
||
file))
|
||
(buffer-list))))
|
||
|
||
(defun denote--get-all-used-ids ()
|
||
"Return a hash-table of all used identifiers.
|
||
It checks files in variable `denote-directory' and active buffer files."
|
||
(let* ((ids (make-hash-table :test #'equal))
|
||
(file-names (mapcar
|
||
(lambda (file) (file-name-nondirectory file))
|
||
(denote-directory-files nil nil nil nil :has-identifier)))
|
||
(names (append file-names (denote--buffer-file-names))))
|
||
(dolist (name names)
|
||
(when-let* ((id (denote-retrieve-filename-identifier name)))
|
||
(puthash id t ids)))
|
||
ids))
|
||
|
||
(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 ((current-id id)
|
||
(iteration 0))
|
||
(while (gethash current-id denote-used-identifiers)
|
||
;; 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 (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'.
|
||
|
||
This is a reference function for `denote-get-identifier-function'."
|
||
(let ((denote-used-identifiers (or denote-used-identifiers (denote--get-all-used-ids))))
|
||
(cond ((and initial-identifier
|
||
(not (gethash initial-identifier denote-used-identifiers)))
|
||
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))))))))
|
||
|
||
(defvar denote-command-prompt-history nil
|
||
"Minibuffer history for `denote-command-prompt'.")
|
||
|
||
(defalias 'denote--command-prompt-history 'denote-command-prompt-history
|
||
"Compatibility alias for `denote-command-prompt-history'.")
|
||
|
||
(defun denote-command-prompt ()
|
||
"Prompt for command among `denote-commands-for-new-notes'."
|
||
(let ((default (car denote-command-prompt-history)))
|
||
(intern
|
||
(completing-read
|
||
(format-prompt "Run note-creating Denote command" default)
|
||
(denote-get-completion-table denote-commands-for-new-notes '(category . command))
|
||
nil t nil 'denote-command-prompt-history default))))
|
||
|
||
;;;;; The `denote' command and its prompts
|
||
|
||
(defun denote--prompt-with-completion-p (fn)
|
||
"Return non-nil if FN prompt should perform completion.
|
||
FN is one among `denote-prompts-with-history-as-completion' and performs
|
||
completion when the user option `denote-history-completion-in-prompts'
|
||
is non-nil."
|
||
(and denote-history-completion-in-prompts
|
||
(memq fn denote-prompts-with-history-as-completion)))
|
||
|
||
(defvar denote-ignore-region-in-denote-command nil
|
||
"If non-nil, the region is ignored by the `denote' command.
|
||
|
||
The `denote' command uses the region as the default title when
|
||
prompted for a title. When this variable is non-nil, the
|
||
`denote' command ignores the region. This variable is useful in
|
||
commands that have their own way of handling the region.")
|
||
|
||
(defvar denote-title-prompt-current-default nil
|
||
"Currently bound default title for `denote-title-prompt'.
|
||
Set the value of this variable within the lexical scope of a
|
||
command that needs to supply a default title before calling
|
||
`denote-title-prompt'.")
|
||
|
||
(defun denote--command-with-features (command force-use-file-prompt-as-default-title force-ignore-region force-save in-background)
|
||
"Execute file-creating COMMAND with specified features.
|
||
|
||
COMMAND is the symbol of a file-creating command to call, such as
|
||
`denote' or `denote-signature'.
|
||
|
||
With non-nil FORCE-USE-FILE-PROMPT-AS-DEFAULT-TITLE, use the last item
|
||
of `denote-file-history' as the default title of the title prompt. This
|
||
is useful in a command such as `denote-link' where the entry of the file
|
||
prompt can be reused as the default title.
|
||
|
||
With non-nil FORCE-IGNORE-REGION, ignore the region when creating the
|
||
note: do not use its text as the initial title in a title prompt. Else,
|
||
do whatever `denote-ignore-region-in-denote-command' entails.
|
||
|
||
With non-nil FORCE-SAVE, save the file at the end of the note creation.
|
||
Else, do whatever the value of `denote-save-buffers' entails.
|
||
|
||
With non-nil IN-BACKGROUND, create the note in the background: do not
|
||
display the note's buffer after it is created.
|
||
|
||
Note that if all parameters except COMMAND are nil, this is
|
||
equivalent to `(call-interactively command)'.
|
||
|
||
Return the path of the newly created file."
|
||
(let ((denote-save-buffers
|
||
(or force-save denote-save-buffers))
|
||
(denote-ignore-region-in-denote-command
|
||
(or force-ignore-region denote-ignore-region-in-denote-command))
|
||
(denote-title-prompt-current-default
|
||
(if force-use-file-prompt-as-default-title
|
||
denote-file-prompt-latest-input
|
||
denote-title-prompt-current-default))
|
||
(path))
|
||
(if in-background
|
||
(save-window-excursion
|
||
(setq path (call-interactively command)))
|
||
(setq path (call-interactively command)))
|
||
path))
|
||
|
||
(defun denote--handle-save-and-kill-buffer (mode file initial-state)
|
||
"Save and kill buffer of FILE according to MODE and INITIAL-STATE.
|
||
|
||
The values of `denote-save-buffers' and `denote-kill-buffers' are
|
||
used to decide whether to save and/or kill the buffer visiting
|
||
FILE.
|
||
|
||
MODE is one of the symbols `creation' or `rename'.
|
||
|
||
INITIAL-STATE is nil or one of the following symbols:
|
||
`not-visited', `visited'. If a buffer was already visited at the
|
||
beginning of a rename operation, it is NOT killed automatically.
|
||
|
||
If a buffer needs to be killed, it is also automatically saved,
|
||
no matter the value of `denote-save-buffers'."
|
||
(let* ((do-kill-buffer (and (not (eq initial-state 'visited))
|
||
(or (eq denote-kill-buffers t)
|
||
(and (eq mode 'creation)
|
||
(eq denote-kill-buffers 'on-creation))
|
||
(and (eq mode 'rename)
|
||
(eq denote-kill-buffers 'on-rename)))))
|
||
(do-save-buffer (or do-kill-buffer denote-save-buffers)))
|
||
(when-let* ((buffer (find-buffer-visiting file)))
|
||
(when do-save-buffer (with-current-buffer buffer (save-buffer)))
|
||
(when do-kill-buffer (kill-buffer buffer)))))
|
||
|
||
(defvar denote-current-data nil
|
||
"Store the current unprocessed data passed to `denote'.
|
||
This is an alist where each `car' is one among `title', `keywords',
|
||
`signature', `directory', `date', `id', `file-type', `template'. The
|
||
value each of them contains is the unprocessed input (e.g. the title
|
||
before it is sluggified).
|
||
|
||
This may be used by the hooks `denote-after-new-note-hook' and
|
||
`denote-after-rename-file-hook' to access the relevant data.")
|
||
|
||
(defvar denote-use-title nil
|
||
"The title 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 title will always be the same
|
||
and the title prompt will be skipped.")
|
||
|
||
(defvar denote-use-keywords 'default
|
||
"The keywords to be used in a note creation command.
|
||
See the documentation of `denote' for acceptable values. This variable
|
||
is ignored if `default'.
|
||
|
||
Only ever `let' bind this, otherwise the keywords will always be the same
|
||
and the keywords prompt will be skipped.")
|
||
|
||
(defvar denote-use-signature nil
|
||
"The signature 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 signature will always be the same
|
||
and the signature prompt will be skipped.")
|
||
|
||
(defvar denote-use-file-type nil
|
||
"The title 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 file type will always be the
|
||
same.")
|
||
|
||
(defvar denote-use-directory nil
|
||
"The directory 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 directory will always be the
|
||
same.")
|
||
|
||
(defvar denote-use-date nil
|
||
"The date 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 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
|
||
is ignored if nil.
|
||
|
||
Only ever `let' bind this, otherwise the template will always be the same
|
||
and the template prompt will be skipped.")
|
||
|
||
(defvar denote-get-identifier-function #'denote-generate-identifier-as-date
|
||
"The function to generate an identifier as a non-empty string.
|
||
|
||
The function takes two arguments: an initial identifier and a date. The
|
||
initial identifier is used as a reference to derive a unique variant of
|
||
it (e.g. to keep incrementing seconds while keeping the rest of the
|
||
date+time the same). Existing identifiers are stored in the variable
|
||
`denote-used-identifiers'. If the initial identifier is nil or an
|
||
identifier cannot be derived from it, then the date can be used instead.
|
||
The date has the same format as `current-time'. When it is nil, the
|
||
`current-time' is used.")
|
||
|
||
(defun denote--creation-get-note-data-from-prompts ()
|
||
"Retrieve the data necessary for note creation.
|
||
|
||
The data elements are: title, keywords, file-type, directory,
|
||
date, template and signature.
|
||
|
||
It is retrieved from prompts according to `denote-prompts' and
|
||
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 identifier template signature)
|
||
(dolist (prompt denote-prompts)
|
||
(pcase prompt
|
||
('title (unless denote-use-title
|
||
(setq title (denote-title-prompt
|
||
(when (and (not denote-ignore-region-in-denote-command)
|
||
(use-region-p))
|
||
(buffer-substring-no-properties
|
||
(region-beginning)
|
||
(region-end)))))))
|
||
('keywords (when (eq denote-use-keywords 'default)
|
||
(setq keywords (denote-keywords-prompt))))
|
||
('file-type (unless denote-use-file-type
|
||
(setq file-type (denote-file-type-prompt))))
|
||
('subdirectory (unless denote-use-directory
|
||
(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 identifier 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,
|
||
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."
|
||
(let* (;; Handle the `denote-use-*' variables
|
||
(title (or denote-use-title title))
|
||
(keywords (if (eq denote-use-keywords 'default) keywords denote-use-keywords))
|
||
(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
|
||
(title (or title ""))
|
||
(file-type (denote--valid-file-type (or file-type denote-file-type)))
|
||
(keywords (denote-keywords-sort keywords))
|
||
(date (denote-valid-date-p date))
|
||
(date (cond (date date)
|
||
(denote-accept-nil-date date)
|
||
(t (current-time))))
|
||
(identifier (or identifier ""))
|
||
(identifier (cond ((not (string-empty-p identifier))
|
||
(funcall denote-get-identifier-function identifier nil))
|
||
((or (eq denote-generate-identifier-automatically t)
|
||
(eq denote-generate-identifier-automatically 'on-creation))
|
||
(funcall denote-get-identifier-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))))
|
||
(template (if (or (stringp template) (functionp template))
|
||
template
|
||
(or (alist-get template denote-templates) "")))
|
||
(signature (or signature "")))
|
||
(list title keywords file-type directory date identifier template signature)))
|
||
|
||
;;;###autoload
|
||
(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
|
||
return its path. Before returning the path, determine what needs to be
|
||
done to the buffer, in accordance with the user option `denote-kill-buffers'.
|
||
|
||
When called interactively, the metadata and file name are prompted
|
||
according to the value of `denote-prompts'.
|
||
|
||
When called from Lisp, all arguments are optional.
|
||
|
||
- TITLE is a string or a function returning a string.
|
||
|
||
- KEYWORDS is a list of strings. The list can be empty or the
|
||
value can be set to nil.
|
||
|
||
- FILE-TYPE is a symbol among those described in the user option
|
||
`denote-file-type'.
|
||
|
||
- DIRECTORY is a string representing the path to either the
|
||
value of the variable `denote-directory' or a subdirectory
|
||
thereof. The subdirectory must exist: Denote will not create
|
||
it. If DIRECTORY does not resolve to a valid path, the first
|
||
item in the variable `denote-directory' is used instead.
|
||
|
||
- DATE is a string representing a date like 2022-06-30 or a date
|
||
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."
|
||
(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
|
||
(cons 'title title)
|
||
(cons 'keywords keywords)
|
||
(cons 'signature signature)
|
||
(cons 'directory directory)
|
||
(cons 'date date)
|
||
(cons 'id identifier)
|
||
(cons 'file-type file-type)
|
||
(cons 'template template)))
|
||
(run-hooks 'denote-after-new-note-hook)
|
||
(denote--handle-save-and-kill-buffer 'creation note-path nil)
|
||
note-path))
|
||
|
||
(defvar denote-title-history nil
|
||
"Minibuffer history of `denote-title-prompt'.")
|
||
|
||
(defalias 'denote--title-history 'denote-title-history
|
||
"Compatibility alias for `denote-title-history'.")
|
||
|
||
(defmacro denote--with-conditional-completion (fn prompt history &optional initial-value default-value)
|
||
"Produce body of FN that may perform completion.
|
||
Use PROMPT, HISTORY, INITIAL-VALUE, and DEFAULT-VALUE as arguments for
|
||
the given minibuffer prompt."
|
||
`(if (denote--prompt-with-completion-p ,fn)
|
||
;; NOTE 2023-10-27: By default SPC performs completion in the
|
||
;; minibuffer. We do not want that, as the user should be able to
|
||
;; input an arbitrary string, while still performing completion
|
||
;; against their input history.
|
||
(minibuffer-with-setup-hook
|
||
(lambda ()
|
||
(use-local-map
|
||
(let ((map (make-composed-keymap nil (current-local-map))))
|
||
(define-key map (kbd "SPC") nil)
|
||
map)))
|
||
(completing-read ,prompt ,history nil nil ,initial-value ',history ,default-value))
|
||
(read-string ,prompt ,initial-value ',history ,default-value)))
|
||
|
||
(defun denote-title-prompt (&optional initial-title prompt-text)
|
||
"Prompt for title string.
|
||
|
||
With optional INITIAL-TITLE 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."
|
||
(denote--with-conditional-completion
|
||
'denote-title-prompt
|
||
(format-prompt (or prompt-text "New file TITLE") denote-title-prompt-current-default)
|
||
denote-title-history
|
||
(or initial-title denote-title-prompt-current-default)
|
||
denote-title-prompt-current-default))
|
||
|
||
(defvar denote-file-type-history nil
|
||
"Minibuffer history of `denote-file-type-prompt'.")
|
||
|
||
(defalias 'denote--file-type-history 'denote-file-type-history
|
||
"Compatibility alias for `denote-file-type-history'.")
|
||
|
||
(defun denote-file-type-prompt ()
|
||
"Prompt for variable `denote-file-type'.
|
||
Note that a non-nil value other than `text', `markdown-yaml', and
|
||
`markdown-toml' falls back to an Org file type. We use `org'
|
||
here for clarity."
|
||
(completing-read
|
||
"Select file TYPE: " (denote--file-type-keys) nil t
|
||
nil 'denote-file-type-history))
|
||
|
||
(defvar denote-date-history nil
|
||
"Minibuffer history of `denote-date-prompt'.")
|
||
|
||
(defalias 'denote--date-history 'denote-date-history
|
||
"Compatibility alias for `denote-date-history'.")
|
||
|
||
(declare-function org-read-date "org" (&optional with-time to-time from-string prompt default-time default-input inactive))
|
||
|
||
(defun denote--date-convert (date prefer-type)
|
||
"Determine how to convert DATE to PREFER-TYPE `:list' or `:string'."
|
||
(unless (memq prefer-type '(:list :string))
|
||
(error "The PREFER-TYPE must be either `:list' or `:string'"))
|
||
(cond ((eq prefer-type :list)
|
||
date)
|
||
((eq prefer-type :string)
|
||
(if date (format-time-string "%F %T" date) ""))))
|
||
|
||
(defun denote-date-prompt (&optional initial-date prompt-text)
|
||
"Prompt for date, expecting YYYY-MM-DD or that plus HH:MM.
|
||
Use Org's more advanced date selection utility if the user option
|
||
`denote-date-prompt-use-org-read-date' is non-nil.
|
||
|
||
With optional INITIAL-DATE use it as the initial minibuffer
|
||
text. With optional PROMPT-TEXT use it in the minibuffer instead
|
||
of the default prompt.
|
||
|
||
INITIAL-DATE is a string that can be processed by `denote-valid-date-p',
|
||
a value that can be parsed by `decode-time' or nil."
|
||
(let ((initial-date (denote-valid-date-p initial-date)))
|
||
(if (and denote-date-prompt-use-org-read-date
|
||
(require 'org nil :no-error))
|
||
(let* ((time (org-read-date nil t nil prompt-text (denote--date-convert initial-date :list)))
|
||
(org-time-seconds (format-time-string "%S" time))
|
||
(cur-time-seconds (format-time-string "%S" (current-time))))
|
||
;; When the user does not input a time, org-read-date defaults to 00 for seconds.
|
||
;; When the seconds are 00, we add the current seconds to avoid identifier collisions.
|
||
(when (string-equal "00" org-time-seconds)
|
||
(setq time (time-add time (string-to-number cur-time-seconds))))
|
||
(format-time-string "%Y-%m-%d %H:%M:%S" time))
|
||
(read-string
|
||
(or prompt-text "DATE and TIME for note (e.g. 2022-06-16 14:30): ")
|
||
(denote--date-convert initial-date :string)
|
||
'denote-date-history))))
|
||
|
||
(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'.")
|
||
|
||
(defalias 'denote--subdir-history 'denote-subdirectory-history
|
||
"Compatibility alias for `denote-subdirectory-history'.")
|
||
|
||
;; TODO 2025-12-14: Explore if we can have relative paths here. The
|
||
;; problem is that we also return the root `denote-directory', so how
|
||
;; should that be presented? Maybe as "."?
|
||
(defun denote-subdirectory-prompt ()
|
||
"Prompt for subdirectory of the variable `denote-directory'.
|
||
The table uses the `file' completion category (so it works with
|
||
packages such as `marginalia' and `embark')."
|
||
(let* ((default (car denote-subdirectory-history))
|
||
(roots (denote-directories))
|
||
(single-dir-p (null (cdr roots)))
|
||
;; Some external program may use `default-directory' with the
|
||
;; relative file paths of the completion candidates.
|
||
(default-directory (if single-dir-p
|
||
(car roots)
|
||
(denote-directories-get-common-root)))
|
||
(subdirectories (denote-directory-subdirectories))
|
||
(directories (append roots subdirectories)))
|
||
(completing-read
|
||
(format-prompt "Select SUBDIRECTORY" default)
|
||
(denote-get-completion-table directories '(category . file))
|
||
nil t nil 'denote-subdirectory-history default)))
|
||
|
||
(defvar denote-template-history nil
|
||
"Minibuffer history of `denote-template-prompt'.")
|
||
|
||
(defalias 'denote--template-history 'denote-template-history
|
||
"Compatibility alias for `denote-template-history'.")
|
||
|
||
(defun denote-template-prompt ()
|
||
"Prompt for template key in `denote-templates' and return its value."
|
||
(let ((templates denote-templates))
|
||
(alist-get
|
||
(intern
|
||
(completing-read
|
||
"Select TEMPLATE key: " (mapcar #'car templates)
|
||
nil t nil 'denote-template-history))
|
||
templates)))
|
||
|
||
(defvar denote-identifier-history nil
|
||
"Minibuffer history of `denote-identifier-prompt'.")
|
||
|
||
(defun denote-identifier-prompt (&optional initial-identifier prompt-text)
|
||
"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."
|
||
(when (and initial-identifier (string-empty-p initial-identifier))
|
||
(setq initial-identifier nil))
|
||
(denote--with-conditional-completion
|
||
'denote-identifier-prompt
|
||
(format-prompt (or prompt-text "New file IDENTIFIER") nil)
|
||
denote-identifier-history
|
||
initial-identifier))
|
||
|
||
(defvar denote-signature-history nil
|
||
"Minibuffer history of `denote-signature-prompt'.")
|
||
|
||
(defalias 'denote--signature-history 'denote-signature-history
|
||
"Compatibility alias for `denote-signature-history'.")
|
||
|
||
(defun denote-signature-prompt (&optional initial-signature prompt-text)
|
||
"Prompt for signature string.
|
||
With optional INITIAL-SIGNATURE 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."
|
||
(when (and initial-signature (string-empty-p initial-signature))
|
||
(setq initial-signature nil))
|
||
(denote--with-conditional-completion
|
||
'denote-signature-prompt
|
||
(format-prompt (or prompt-text "New file SIGNATURE") nil)
|
||
denote-signature-history
|
||
initial-signature))
|
||
|
||
(defvar denote-files-matching-regexp-history nil
|
||
"Minibuffer history of `denote-files-matching-regexp-prompt'.")
|
||
|
||
(defalias 'denote--files-matching-regexp-hist 'denote-files-matching-regexp-history
|
||
"Compatibility alias for `denote-files-matching-regexp-history'.")
|
||
|
||
(defun denote-files-matching-regexp-prompt (&optional prompt-text)
|
||
"Prompt for REGEXP to filter Denote files by.
|
||
With optional PROMPT-TEXT use it instead of a generic prompt."
|
||
(denote--with-conditional-completion
|
||
'denote-files-matching-regexp-prompt
|
||
(format-prompt (or prompt-text "Match files with the given REGEXP") nil)
|
||
denote-files-matching-regexp-history))
|
||
|
||
;;;;; Convenience commands as `denote' variants
|
||
|
||
(defalias 'denote-create-note 'denote
|
||
"Alias for `denote' command.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--add-prompts
|
||
'denote-add-prompts
|
||
"3.0.0")
|
||
|
||
(defun denote-add-prompts (additional-prompts)
|
||
"Add list of ADDITIONAL-PROMPTS to `denote-prompts'.
|
||
This is best done inside of a `let' to create a wrapper function around
|
||
`denote', `denote-rename-file', and generally any command that consults
|
||
the value of `denote-prompts'."
|
||
(seq-union additional-prompts denote-prompts))
|
||
|
||
;;;###autoload
|
||
(defun denote-type ()
|
||
"Create note while prompting for a file type.
|
||
|
||
This is the equivalent of calling `denote' when `denote-prompts'
|
||
has the `file-type' prompt appended to its existing prompts."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts (denote-add-prompts '(file-type))))
|
||
(call-interactively #'denote)))
|
||
|
||
(defalias 'denote-create-note-using-type 'denote-type
|
||
"Alias for `denote-type' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-date ()
|
||
"Create note while prompting for a date.
|
||
|
||
The date can be in YEAR-MONTH-DAY notation like 2022-06-30 or
|
||
that plus the time: 2022-06-16 14:30. When the user option
|
||
`denote-date-prompt-use-org-read-date' is non-nil, the date
|
||
prompt uses the more powerful Org+calendar system.
|
||
|
||
This is the equivalent of calling `denote' when `denote-prompts'
|
||
has the `date' prompt appended to its existing prompts."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts (denote-add-prompts '(date))))
|
||
(call-interactively #'denote)))
|
||
|
||
(defalias 'denote-create-note-using-date 'denote-date
|
||
"Alias for `denote-date' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-subdirectory ()
|
||
"Create note while prompting for a subdirectory.
|
||
|
||
Available candidates include the value of the variable
|
||
`denote-directory' and any subdirectory thereof.
|
||
|
||
This is the equivalent of calling `denote' when `denote-prompts'
|
||
has the `subdirectory' prompt appended to its existing prompts."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts (denote-add-prompts '(subdirectory))))
|
||
(call-interactively #'denote)))
|
||
|
||
(defalias 'denote-create-note-in-subdirectory 'denote-subdirectory
|
||
"Alias for `denote-subdirectory' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-template ()
|
||
"Create note while prompting for a template.
|
||
|
||
Available candidates include the keys in the `denote-templates'
|
||
alist. The value of the selected key is inserted in the newly
|
||
created note after the front matter.
|
||
|
||
This is the equivalent of calling `denote' when `denote-prompts'
|
||
has the `template' prompt appended to its existing prompts."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts (denote-add-prompts '(template))))
|
||
(call-interactively #'denote)))
|
||
|
||
(defalias 'denote-create-note-with-template 'denote-template
|
||
"Alias for `denote-template' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-signature ()
|
||
"Create note while prompting for a file signature.
|
||
|
||
This is the equivalent of calling `denote' when `denote-prompts'
|
||
has the `signature' prompt appended to its existing prompts."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts (denote-add-prompts '(signature))))
|
||
(call-interactively #'denote)))
|
||
|
||
(defalias 'denote-create-note-using-signature 'denote-signature
|
||
"Alias for `denote-signature' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-region ()
|
||
"Call `denote' and insert therein the text of the active region.
|
||
|
||
Note that, currently, `denote-save-buffers' and
|
||
`denote-kill-buffers' are NOT respected. The buffer is not
|
||
saved or killed at the end of `denote-region'."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(if-let* (((region-active-p))
|
||
;; We capture the text early, otherwise it will be empty
|
||
;; the moment `insert' is called.
|
||
(text (buffer-substring-no-properties (region-beginning) (region-end))))
|
||
(progn
|
||
(let ((denote-ignore-region-in-denote-command t)
|
||
;; FIXME: Find a way to insert the region before the buffer is
|
||
;; saved/killed by the creation command.
|
||
(denote-save-buffers nil)
|
||
(denote-kill-buffers nil))
|
||
(call-interactively 'denote))
|
||
(push-mark (point))
|
||
(insert text)
|
||
(run-hook-with-args 'denote-region-after-new-note-functions (mark) (point)))
|
||
(call-interactively 'denote)))
|
||
|
||
;;;;; Other convenience commands
|
||
|
||
;;;###autoload
|
||
(defun denote-open-or-create (target)
|
||
"Visit TARGET file in variable `denote-directory'.
|
||
If file does not exist, invoke `denote' to create a file. In that case,
|
||
use the last input at the file prompt as the default value of the title
|
||
prompt."
|
||
(interactive (list (denote-file-prompt nil "Select file (RET on no match to create it)" :no-require-match)))
|
||
(if (and target (file-exists-p target))
|
||
(find-file target)
|
||
(denote--command-with-features #'denote :use-last-input-as-def-title nil nil nil)))
|
||
|
||
;;;###autoload
|
||
(defun denote-open-or-create-with-command ()
|
||
"Like `denote-open-or-create' but use one of the `denote-commands-for-new-notes'."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((target (denote-file-prompt nil "Select file (RET on no match to create it)" :no-require-match)))
|
||
(if (and target (file-exists-p target))
|
||
(find-file target)
|
||
(denote--command-with-features (denote-command-prompt) :use-file-prompt-as-def-title nil nil nil))))
|
||
|
||
;;;; Note modification
|
||
|
||
;;;;; Common helpers for note modifications
|
||
|
||
(defun denote--file-types-with-extension (extension)
|
||
"Return only the entries of `denote-file-types' with EXTENSION.
|
||
See the format of `denote-file-types'."
|
||
(seq-filter (lambda (type)
|
||
(string-equal (plist-get (cdr type) :extension) extension))
|
||
denote-file-types))
|
||
|
||
(defun denote--file-type-org-extra-p ()
|
||
"Return non-nil if this is an `org-capture' or Org Note buffer."
|
||
(and (derived-mode-p 'org-mode)
|
||
(or (and (bound-and-true-p org-capture-mode)
|
||
(string-match-p "\\`CAPTURE-.*" (buffer-name)))
|
||
(string-match-p "\\`\\*Org Note\\*" (buffer-name))
|
||
(null buffer-file-name))))
|
||
|
||
(defun denote-file-type (file)
|
||
"Use the file extension to detect the file type of FILE.
|
||
Do so in accordance with `denote-file-types'.
|
||
|
||
If more than one file type correspond to this file extension, return the
|
||
first file type whose `:get-file-type-function' returns non-nil. If
|
||
`:get-file-type-function' is nil rely on the `:title-key-regexp' and
|
||
return the first matching file type.
|
||
|
||
Return nil if FILE is not recognized."
|
||
(when-let* ((extension (denote-get-file-extension-sans-encryption file))
|
||
(types (denote--file-types-with-extension extension))
|
||
(length (length types)))
|
||
(cond
|
||
((when-let* ((_ (> length 1))
|
||
(found (seq-find
|
||
(lambda (type)
|
||
(let ((properties (cdr type)))
|
||
(if-let* ((file-type-fn (plist-get properties :get-file-type-function)))
|
||
(funcall file-type-fn file)
|
||
(ignore-errors
|
||
(denote--regexp-in-file-p (plist-get properties :title-key-regexp) file)))))
|
||
types)))
|
||
(car found)))
|
||
((and (> length 1)
|
||
(memq denote-file-type (mapcar #'car types)))
|
||
denote-file-type)
|
||
(t
|
||
(caar types)))))
|
||
|
||
(defun denote-filetype-heuristics (file)
|
||
"Return likely file type of FILE.
|
||
If in the process of `org-capture', consider the file type to be that of
|
||
Org. Otherwise, use the function `denote-file-type' to return the type."
|
||
(if (denote--file-type-org-extra-p)
|
||
'org
|
||
(denote-file-type file)))
|
||
|
||
(defun denote--revert-dired (buffer-to-try-revert current-buffer)
|
||
"Maybe revert BUFFER-TO-TRY-REVERT.
|
||
Do it if BUFFER-TO-TRY-REVERT is in Dired mode and is either part of the
|
||
variable `denote-directory' or equal to the CURRENT-BUFFER."
|
||
(with-current-buffer buffer-to-try-revert
|
||
(when (and (eq major-mode 'dired-mode)
|
||
(or (and default-directory (denote--dir-in-denote-directory-p default-directory))
|
||
(eq current-buffer buffer-to-try-revert)))
|
||
(revert-buffer))))
|
||
|
||
(defun denote-update-dired-buffers ()
|
||
"Update Dired buffers of variable `denote-directory'.
|
||
Also revert the current Dired buffer even if it is not inside the
|
||
variable `denote-directory'."
|
||
(let ((current (current-buffer)))
|
||
(dolist (buffer (buffer-list))
|
||
(denote--revert-dired buffer current))))
|
||
|
||
(defun denote-rename-file-and-buffer (old-name new-name)
|
||
"Rename file named OLD-NAME to NEW-NAME, updating buffer name.
|
||
|
||
If the file exists on the file system, it is renamed. This
|
||
function may be called when creating a new note and the file does
|
||
not exist yet.
|
||
|
||
If a buffer is visiting the file, its name is updated."
|
||
(unless (string= (expand-file-name old-name) (expand-file-name new-name))
|
||
(when (and (file-regular-p old-name)
|
||
(file-writable-p new-name))
|
||
(cond
|
||
((derived-mode-p 'dired-mode)
|
||
(dired-rename-file old-name new-name nil))
|
||
;; NOTE 2024-02-25: The `vc-rename-file' requires the file to be
|
||
;; saved, but our convention is to not save the buffer after
|
||
;; changing front matter unless we absolutely have to (allows
|
||
;; users to do `diff-buffer-with-file', for example).
|
||
((and denote-save-buffers (not (buffer-modified-p)) (vc-backend old-name))
|
||
(vc-rename-file old-name new-name))
|
||
(t
|
||
(rename-file old-name new-name nil))))
|
||
(when-let* ((buffer (find-buffer-visiting old-name)))
|
||
(with-current-buffer buffer
|
||
(set-visited-file-name new-name nil t)))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--add-front-matter
|
||
'denote-prepend-front-matter
|
||
"4.0.0")
|
||
|
||
(defun denote-prepend-front-matter (file title keywords signature date id file-type)
|
||
"Prepend front matter to FILE.
|
||
The TITLE, KEYWORDS, DATE, ID, SIGNATURE, and FILE-TYPE are passed from
|
||
the renaming command and are used to construct a new front matter block
|
||
if appropriate."
|
||
(when-let* ((new-front-matter (denote--format-front-matter title date keywords id signature file-type)))
|
||
(with-current-buffer (find-file-noselect file)
|
||
(goto-char (point-min))
|
||
(insert new-front-matter))))
|
||
|
||
(defun denote-rewrite-keywords (file keywords file-type &optional save-buffer)
|
||
"Rewrite KEYWORDS in FILE outright according to FILE-TYPE.
|
||
|
||
Do the same as `denote-rewrite-front-matter' for keywords,
|
||
but do not ask for confirmation.
|
||
|
||
With optional SAVE-BUFFER, save the buffer corresponding to FILE.
|
||
|
||
This function is for use in the commands `denote-keywords-add',
|
||
`denote-keywords-remove', `denote-dired-rename-files', or
|
||
related."
|
||
(let* ((new-front-matter (denote--format-front-matter "" (current-time) keywords "" "" file-type))
|
||
(new-keywords-line (denote--retrieve-front-matter-keywords-line-from-content new-front-matter file-type)))
|
||
(with-current-buffer (find-file-noselect file)
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
(when (denote--rewrite-front-matter-line 'keywords new-keywords-line file-type)
|
||
(when save-buffer (save-buffer))))))))
|
||
|
||
(defun denote--component-has-value-p (component value)
|
||
"Return non-nil if COMPONENT has a non-nil/non-empty VALUE.
|
||
|
||
COMPONENT can be one of `title', `keywords', `signature', `date',
|
||
`identifier'.
|
||
|
||
VALUE is the corresponding value to test.
|
||
|
||
This function returns nil given an empty string title, signature or
|
||
identifier. It also returns nil given a nil date or nil keywords."
|
||
(pcase component
|
||
('title (not (string-empty-p value)))
|
||
('keywords (not (null value)))
|
||
('signature (not (string-empty-p (denote-sluggify-and-apply-rules 'signature value))))
|
||
('date (not (null value)))
|
||
('identifier (not (string-empty-p value)))))
|
||
|
||
(defun denote--get-old-and-new-front-matter-lines (file new-front-matter file-type)
|
||
"Return an alist of the old and new front-matter lines for each component.
|
||
|
||
The FILE contains the old front matter lines.
|
||
|
||
NEW-FRONT-MATTER is a the front matter with the new values, with the
|
||
format given by FILE-TYPE."
|
||
`((title . ((old . ,(denote-retrieve-front-matter-title-line file file-type))
|
||
(new . ,(denote--retrieve-front-matter-title-line-from-content new-front-matter file-type))))
|
||
(keywords . ((old . ,(denote-retrieve-front-matter-keywords-line file file-type))
|
||
(new . ,(denote--retrieve-front-matter-keywords-line-from-content new-front-matter file-type))))
|
||
(signature . ((old . ,(denote-retrieve-front-matter-signature-line file file-type))
|
||
(new . ,(denote--retrieve-front-matter-signature-line-from-content new-front-matter file-type))))
|
||
(date . ((old . ,(denote-retrieve-front-matter-date-line file file-type))
|
||
(new . ,(denote--retrieve-front-matter-date-line-from-content new-front-matter file-type))))
|
||
(identifier . ((old . ,(denote-retrieve-front-matter-identifier-line file file-type))
|
||
(new . ,(denote--retrieve-front-matter-identifier-line-from-content new-front-matter file-type))))))
|
||
|
||
(defun denote--get-front-matter-components-order (content file-type)
|
||
"Return the components in the order they appear in CONTENT given FILE-TYPE.
|
||
|
||
Return a list containing the symbols `title', `signature', `keywords',
|
||
`identifier' and `date' in the order that they appear in TEXT. TEXT can
|
||
be any string. For example, it can be a front matter template or an
|
||
entire file content."
|
||
(let ((components-with-line-numbers '()))
|
||
(with-temp-buffer
|
||
(insert content)
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (denote--title-key-regexp file-type) nil t 1)
|
||
(push `(,(line-number-at-pos) . title) components-with-line-numbers))
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (denote--keywords-key-regexp file-type) nil t 1)
|
||
(push `(,(line-number-at-pos) . keywords) components-with-line-numbers))
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (denote--signature-key-regexp file-type) nil t 1)
|
||
(push `(,(line-number-at-pos) . signature) components-with-line-numbers))
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (denote--date-key-regexp file-type) nil t 1)
|
||
(push `(,(line-number-at-pos) . date) components-with-line-numbers))
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (denote--identifier-key-regexp file-type) nil t 1)
|
||
(push `(,(line-number-at-pos) . identifier) components-with-line-numbers)))
|
||
(mapcar #'cdr
|
||
(sort components-with-line-numbers (lambda (x y) (< (car x) (car y)))))))
|
||
|
||
(defun denote--file-has-front-matter-p (file file-type)
|
||
"Return non-nil if FILE has at least one front-matter line, given FILE-TYPE.
|
||
|
||
This is checked against its front matter definition. If the front matter
|
||
definition has no lines, this function returns non-nil."
|
||
(let* ((front-matter (denote--front-matter file-type))
|
||
(file-content (with-current-buffer (find-file-noselect file) (buffer-string)))
|
||
(components-in-template (denote--get-front-matter-components-order front-matter file-type))
|
||
(components-in-file (denote--get-front-matter-components-order file-content file-type)))
|
||
(or (null components-in-template)
|
||
(seq-intersection components-in-template components-in-file))))
|
||
|
||
(defun denote--get-front-matter-rewrite-prompt (final-components to-add to-remove to-modify old-and-new-front-matter-lines)
|
||
"Return the prompt for the front matter rewrite operation.
|
||
|
||
FINAL-COMPONENTS is the list of components to handle at the end of the
|
||
rewrite operation.
|
||
|
||
TO-ADD, TO-REMOVE, and TO-MODIFY are the list of components that needs
|
||
to be added, removed or modified.
|
||
|
||
OLD-AND-NEW-FRONT-MATTER-LINES is an alist containing the old and new
|
||
front matter lines."
|
||
(let ((prompt "Replace front matter?"))
|
||
(dolist (component final-components)
|
||
(let ((old-line (alist-get 'old (alist-get component old-and-new-front-matter-lines)))
|
||
(new-line (alist-get 'new (alist-get component old-and-new-front-matter-lines)))
|
||
(next-prompt ""))
|
||
(cond ((memq component to-remove)
|
||
(setq next-prompt (format "\n-%s\n"
|
||
(propertize (denote-trim-whitespace old-line) 'face 'denote-faces-prompt-old-name))))
|
||
((memq component to-add)
|
||
(setq next-prompt (format "\n-%s\n"
|
||
(propertize (denote-trim-whitespace new-line) 'face 'denote-faces-prompt-new-name))))
|
||
((memq component to-modify)
|
||
(setq next-prompt (format "\n-%s\n-%s\n"
|
||
(propertize (denote-trim-whitespace old-line) 'face 'denote-faces-prompt-old-name)
|
||
(propertize (denote-trim-whitespace new-line) 'face 'denote-faces-prompt-new-name)))))
|
||
(setq prompt (concat prompt next-prompt))))
|
||
(concat prompt "?")))
|
||
|
||
(defun denote--get-final-components-for-rewrite (components-in-file components-in-template components-to-add)
|
||
"Return the final components to handle by a front matter rewrite operation.
|
||
|
||
COMPONENTS-TO-ADD is the list of components that have to be added to
|
||
COMPONENTS-IN-FILE to build the list of components that will need to be
|
||
handled during a front matter rewrite operation.
|
||
|
||
COMPONENTS-IN-TEMPLATE is the list of components in a front matter
|
||
template. They are used to determine how the COMPONENTS-TO-ADD are
|
||
added to COMPONENTS-IN-FILE.
|
||
|
||
Example:
|
||
file = (title signature)
|
||
template = (title keywords date id signature)
|
||
|
||
The date line is missing from the file. From the template, we find out
|
||
that it needs to be added *after* a keywords line. Since we don't have
|
||
one in the file, we keep looking for a line to add it *after* and find a
|
||
title line. Had we not found the title line in the file, we would have
|
||
searched for a line to insert it *before*. We would have inserted the
|
||
date line before the signature line, for example.
|
||
|
||
This is repeated until all missing components are added."
|
||
(let ((final-components (copy-sequence components-in-file)))
|
||
(dolist (component components-to-add)
|
||
(if-let* ((previous-components-in-template
|
||
(seq-take-while (lambda (x) (not (eq x component))) components-in-template))
|
||
(first-previous-component-in-file
|
||
(seq-find (lambda (x) (memq x final-components)) (reverse previous-components-in-template))))
|
||
;; Insert after the existing element.
|
||
(let ((sublist final-components))
|
||
(while sublist
|
||
(if (not (eq (car sublist) first-previous-component-in-file))
|
||
(setq sublist (cdr sublist))
|
||
(push component (cdr sublist))
|
||
(setq sublist nil))))
|
||
(let* ((next-components-in-template
|
||
(cdr (seq-drop-while (lambda (x) (not (eq x component))) components-in-template)))
|
||
(first-next-component-in-file
|
||
(seq-find (lambda (x) (memq x final-components)) next-components-in-template)))
|
||
;; Insert before the existing element. The intention is to
|
||
;; modify final-components, but it does not work when push
|
||
;; is called on sublist on the first iteration of the loop.
|
||
(if (eq (car final-components) first-next-component-in-file)
|
||
(push component final-components)
|
||
(let ((sublist final-components))
|
||
(while sublist
|
||
(if (not (eq (car sublist) first-next-component-in-file))
|
||
(setq sublist (cdr sublist))
|
||
(push component sublist)
|
||
(setq sublist nil))))))))
|
||
final-components))
|
||
|
||
(defun denote-rewrite-front-matter (file title keywords signature date identifier file-type)
|
||
"Rewrite front matter of note after `denote-rename-file'.
|
||
The FILE, TITLE, KEYWORDS, SIGNATURE, DATE, IDENTIFIER, and FILE-TYPE
|
||
are given by the renaming command and are used to construct new front
|
||
matter values if appropriate.
|
||
|
||
If `denote-rename-confirmations' contains `rewrite-front-matter',
|
||
prompt to confirm the rewriting of the front matter."
|
||
(let* ((front-matter (denote--front-matter file-type))
|
||
(file-content (with-current-buffer (find-file-noselect file) (buffer-string)))
|
||
(components-in-template (denote--get-front-matter-components-order front-matter file-type))
|
||
(components-in-file (denote--get-front-matter-components-order file-content file-type))
|
||
(components-to-add '())
|
||
(components-to-remove '())
|
||
(components-to-modify '())
|
||
(new-front-matter (denote--format-front-matter title date keywords identifier signature file-type))
|
||
(old-and-new-front-matter-lines (denote--get-old-and-new-front-matter-lines file new-front-matter file-type)))
|
||
;; Build the lists of components to add, remove, modify.
|
||
(dolist (component '(title keywords signature identifier date))
|
||
;; Ignore the component if it is not in the template. It is not added, removed or modified.
|
||
(when (memq component components-in-template)
|
||
(let ((value (pcase component ('title title) ('keywords keywords) ('signature signature) ('date date) ('identifier identifier))))
|
||
(cond ((and (not (memq component components-in-file))
|
||
(denote--component-has-value-p component value))
|
||
(push component components-to-add))
|
||
((and (memq component components-in-file)
|
||
;; The component can still be marked for modification.
|
||
(not (memq component denote-front-matter-components-present-even-if-empty-value))
|
||
(not (denote--component-has-value-p component value)))
|
||
(push component components-to-remove))
|
||
((and (memq component components-in-file)
|
||
(not (string= (alist-get 'old (alist-get component old-and-new-front-matter-lines))
|
||
(alist-get 'new (alist-get component old-and-new-front-matter-lines)))))
|
||
(push component components-to-modify))))))
|
||
;; There should be at least one component in the file and the template.
|
||
(when (and (seq-intersection components-in-file components-in-template)
|
||
(or components-to-add components-to-remove components-to-modify))
|
||
(when-let* ((final-components (denote--get-final-components-for-rewrite
|
||
components-in-file components-in-template components-to-add)))
|
||
(with-current-buffer (find-file-noselect file)
|
||
(when (or (not (memq 'rewrite-front-matter denote-rename-confirmations))
|
||
(y-or-n-p (denote--get-front-matter-rewrite-prompt
|
||
final-components
|
||
components-to-add components-to-remove components-to-modify
|
||
old-and-new-front-matter-lines)))
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
;; Position point at the beginning of the first front matter line
|
||
(let ((first-component (car (seq-difference final-components components-to-add))))
|
||
(re-search-forward
|
||
(funcall (denote--get-component-key-regexp-function first-component) file-type) nil t 1)
|
||
(goto-char (line-beginning-position)))
|
||
;; Do the modifications
|
||
(dolist (component final-components)
|
||
(let ((component-key-regexp-function (denote--get-component-key-regexp-function component))
|
||
(new-line (alist-get 'new (alist-get component old-and-new-front-matter-lines))))
|
||
(cond ((memq component components-to-remove)
|
||
(re-search-forward (funcall component-key-regexp-function file-type) nil t 1)
|
||
(delete-region (line-beginning-position) (line-beginning-position 2)))
|
||
((memq component components-to-add)
|
||
(insert (concat new-line "\n")))
|
||
((memq component components-to-modify)
|
||
(denote--rewrite-front-matter-line component new-line file-type)
|
||
(goto-char (line-beginning-position 2)))
|
||
(t
|
||
(goto-char (line-beginning-position 2))))))))))))))
|
||
|
||
;;;;; The renaming commands and their prompts
|
||
|
||
(defun denote--rename-dired-file-or-current-file-or-prompt ()
|
||
"Return Dired file at point or the current file, else prompt for one.
|
||
Throw error if FILE is not regular, else return FILE."
|
||
(or (dired-get-filename nil t)
|
||
buffer-file-name
|
||
(let* ((file (buffer-file-name))
|
||
(format (if file
|
||
(format "Rename FILE Denote-style [%s]: " file)
|
||
"Rename FILE Denote-style: "))
|
||
(selected-file (read-file-name format nil file t nil)))
|
||
(if (or (file-directory-p selected-file)
|
||
(not (file-regular-p selected-file)))
|
||
(user-error "Only rename regular files")
|
||
selected-file))))
|
||
|
||
(defun denote-rename-file-prompt (old-name new-name)
|
||
"Prompt to rename file named OLD-NAME to NEW-NAME.
|
||
Return non-nil if the file should be renamed.
|
||
|
||
If `denote-rename-confirmations' does not contain
|
||
`modify-file-name', return t without prompting."
|
||
(or (not (memq 'modify-file-name denote-rename-confirmations))
|
||
(unless (string= (expand-file-name old-name) (expand-file-name new-name))
|
||
(y-or-n-p
|
||
(format "Rename %s to %s?"
|
||
(propertize (file-name-nondirectory old-name) 'face 'denote-faces-prompt-old-name)
|
||
(propertize (file-name-nondirectory new-name) 'face 'denote-faces-prompt-new-name))))))
|
||
|
||
(defun denote-add-front-matter-prompt (file)
|
||
"Prompt to add a front-matter to FILE.
|
||
Return non-nil if a new front matter should be added.
|
||
|
||
If `denote-rename-confirmations' does not contain
|
||
`add-front-matter', return t without prompting."
|
||
(or (not (memq 'add-front-matter denote-rename-confirmations))
|
||
(y-or-n-p
|
||
(format "Add new front matter to %s?"
|
||
(propertize (file-name-nondirectory file) 'face 'denote-faces-prompt-new-name)))))
|
||
|
||
;; NOTE 2023-10-20: We do not need a user option for this, though it
|
||
;; can be useful to have it as a variable.
|
||
(defvar denote-rename-max-mini-window-height 0.33
|
||
"How much to enlarge `max-mini-window-height' for renaming operations.")
|
||
|
||
(defun denote--generate-date-for-rename (file)
|
||
"Generate a date for FILE.
|
||
|
||
Respect `denote-generate-identifier-automatically'."
|
||
(if (or (eq denote-generate-identifier-automatically t)
|
||
(eq denote-generate-identifier-automatically 'on-rename))
|
||
(or (file-attribute-modification-time (file-attributes file))
|
||
(current-time))
|
||
nil))
|
||
|
||
(defvar denote-rename-rewrite-front-matter t
|
||
"When non-nil, rewrite the front matter if appropriate.
|
||
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 identifier)
|
||
"Rename FILE according to the other parameters.
|
||
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
|
||
according to its parameters. In particular, it does not prompt
|
||
for anything. It is meant to be combined with
|
||
`denote--rename-get-file-info-from-prompts-or-existing' to create
|
||
a renaming command.
|
||
|
||
Respect `denote-rename-confirmations', `denote-save-buffers' and
|
||
`denote-kill-buffers'."
|
||
(let* ((initial-state (if (find-buffer-visiting file) 'visited 'not-visited))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(keywords (denote-keywords-sort keywords))
|
||
(directory (file-name-directory file))
|
||
(extension (denote-get-file-extension file))
|
||
(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-get-identifier-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)
|
||
((and (not (string-empty-p old-identifier)) (denote--file-has-backlinks-p file))
|
||
(user-error "The identifier cannot be modified: that will break existing links"))
|
||
(t (funcall denote-get-identifier-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))
|
||
(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))
|
||
;; Modify file name, buffer name, or both
|
||
(when (denote-rename-file-prompt file new-name)
|
||
(denote-rename-file-and-buffer file new-name))
|
||
;; Handle front matter if new-name is of a supported type (rewrite or add front matter)
|
||
(when (and denote-rename-rewrite-front-matter
|
||
(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 identifier file-type)
|
||
(when (denote-add-front-matter-prompt new-name)
|
||
(denote-prepend-front-matter new-name title keywords signature date identifier file-type))))
|
||
(when (and denote-used-identifiers (not (string-empty-p identifier)))
|
||
(puthash identifier t denote-used-identifiers))
|
||
(denote--handle-save-and-kill-buffer 'rename new-name initial-state)
|
||
(setq denote-current-data
|
||
(list
|
||
(cons 'title title)
|
||
(cons 'keywords keywords)
|
||
(cons 'signature signature)
|
||
(cons 'directory directory)
|
||
(cons 'date date)
|
||
(cons 'id identifier)
|
||
(cons 'file-type file-type)
|
||
(cons 'template "")))
|
||
(run-hooks 'denote-after-rename-file-hook)
|
||
new-name))
|
||
|
||
(defun denote--rename-get-file-info-from-prompts-or-existing (file)
|
||
"Retrieve existing info from FILE and prompt according to `denote-prompts'.
|
||
|
||
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))
|
||
(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) "")))
|
||
(dolist (prompt denote-prompts)
|
||
(pcase prompt
|
||
('title
|
||
(setq title (denote-title-prompt
|
||
title
|
||
(format "Rename `%s' with TITLE (empty to remove)" file-in-prompt))))
|
||
('keywords
|
||
(setq keywords (denote-keywords-prompt
|
||
(format "Rename `%s' with KEYWORDS (empty to remove)" file-in-prompt)
|
||
(string-join keywords ","))))
|
||
('signature
|
||
(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 identifier)))
|
||
|
||
;;;###autoload
|
||
(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:
|
||
never move it to another directory.
|
||
|
||
If in Dired, consider FILE to be the one at point, else the
|
||
current file, else prompt with minibuffer completion for one.
|
||
When called from Lisp, FILE is a file system path represented as
|
||
a string.
|
||
|
||
If FILE has a Denote-compliant identifier, retain it while
|
||
updating components of the file name referenced by the user
|
||
option `denote-prompts'. By default, these are the TITLE and
|
||
KEYWORDS. The SIGNATURE is another one. When called from Lisp,
|
||
TITLE and SIGNATURE are strings, while KEYWORDS is a list of
|
||
strings.
|
||
|
||
The IDENTIFIER is a string that has the format of variable
|
||
`denote-date-identifier-format'.
|
||
|
||
If there is no identifier, create a new identifier using
|
||
`denote-get-identifier-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
|
||
minibuffer that consists of the current title of FILE. The
|
||
current title is either retrieved from the front matter (such as
|
||
the #+title in Org) or from the file name.
|
||
|
||
Do the same for the SIGNATURE prompt, subject to `denote-prompts',
|
||
by prefilling the minibuffer with the current signature of FILE,
|
||
if any.
|
||
|
||
Same principle for the KEYWORDS prompt: convert the keywords in
|
||
the file name into a comma-separated string and prefill the
|
||
minibuffer with it (the KEYWORDS prompt accepts more than one
|
||
keywords, each separated by a comma, else the `crm-separator').
|
||
|
||
For all prompts, interpret an empty input as an instruction to
|
||
remove that file name component. For example, if a TITLE prompt
|
||
is available and FILE is 20240211T093531--some-title__keyword1.org
|
||
then rename FILE to 20240211T093531__keyword1.org.
|
||
|
||
In interactive use, if there is no entry for a file name
|
||
component in `denote-prompts', keep it as-is.
|
||
|
||
When called from Lisp, the special symbol `keep-current' can be
|
||
used for the TITLE, KEYWORDS, SIGNATURE, DATE, and IDENTIFIER
|
||
parameters to keep them as-is.
|
||
|
||
[ NOTE: Please check with your minibuffer user interface how to
|
||
provide an empty input. The Emacs default setup accepts the
|
||
empty minibuffer contents as they are, though popular packages
|
||
like `vertico' use the first available completion candidate
|
||
instead. For `vertico', the user must either move one up to
|
||
select the prompt and then type RET there with empty contents,
|
||
or use the command `vertico-exit-input' with empty contents.
|
||
That Vertico command is bound to M-RET as of this writing on
|
||
2024-02-13 08:08 +0200. ]
|
||
|
||
As a final step, ask for confirmation, showing the difference
|
||
between old and new file names. Do not ask for confirmation if
|
||
the user option `denote-rename-confirmations' does not contain
|
||
the symbol `modify-file-name'.
|
||
|
||
If FILE has front matter for TITLE and KEYWORDS, ask to rewrite
|
||
their values in order to reflect the new input, unless
|
||
`denote-rename-confirmations' lacks `rewrite-front-matter'. When
|
||
the `denote-save-buffers' is nil (the default), do not save the
|
||
underlying buffer, thus giving the user the option to
|
||
double-check the result, such as by invoking the command
|
||
`diff-buffer-with-file'. The rewrite of the TITLE and KEYWORDS
|
||
in the front matter should not affect the rest of the front
|
||
matter.
|
||
|
||
If the file does not have front matter but is among the supported file
|
||
types (per the user option `denote-file-type'), add front matter to the
|
||
top of it and leave the buffer unsaved for further inspection. Save the
|
||
buffer if `denote-save-buffers' is non-nil.
|
||
|
||
When `denote-kill-buffers' is t or `on-rename', kill the buffer
|
||
if it was not already being visited before the rename operation.
|
||
|
||
For the front matter of each file type, refer to the variables:
|
||
|
||
- `denote-org-front-matter'
|
||
- `denote-text-front-matter'
|
||
- `denote-toml-front-matter'
|
||
- `denote-yaml-front-matter'
|
||
|
||
Construct the file name in accordance with the user option
|
||
`denote-file-name-components-order'.
|
||
|
||
Run the `denote-after-rename-file-hook' after renaming FILE.
|
||
|
||
This command is intended to (i) rename Denote files, (ii) convert
|
||
existing supported file types to Denote notes, and (ii) rename
|
||
non-note files (e.g. PDF) that can benefit from Denote's
|
||
file-naming scheme.
|
||
|
||
For a version of this command that works with multiple files
|
||
one-by-one, use `denote-dired-rename-files'."
|
||
(interactive
|
||
(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) "")
|
||
title))
|
||
(keywords (if (eq keywords 'keep-current)
|
||
(denote-extract-keywords-from-path file)
|
||
keywords))
|
||
(signature (if (eq signature 'keep-current)
|
||
(or (denote-retrieve-filename-signature file) "")
|
||
signature))
|
||
(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 identifier)))
|
||
(denote-update-dired-buffers)
|
||
new-name))
|
||
|
||
(defun denote-rename-file-title ()
|
||
"Convenience command to change the title of a file.
|
||
Like `denote-rename-file', but prompts only for the title.
|
||
|
||
Add or remove a title in one go. Do this by prepopulating the
|
||
minibuffer prompt with the existing title. The user can then modify it
|
||
accordingly. An empty input means to remove the title 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 '(title)))
|
||
(call-interactively #'denote-rename-file)))
|
||
|
||
(defun denote-rename-file-keywords ()
|
||
"Convenience command to change the keywords of a file.
|
||
Like `denote-rename-file', but prompts only for keywords.
|
||
|
||
Add or remove keywords in one go. Do this by prepopulating the
|
||
minibuffer prompt with the existing keywords. The user can then insert
|
||
the `crm-separator' (normally a comma), to write new keywords or edit
|
||
what is in the prompt to rewrite them accordingly. An empty input means
|
||
to remove all keywords.
|
||
|
||
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 '(keywords)))
|
||
(call-interactively #'denote-rename-file)))
|
||
|
||
(defun denote-rename-file-date ()
|
||
"Convenience command to change the date of a file.
|
||
Like `denote-rename-file', but prompts only for the date.
|
||
|
||
Modify a date in one go."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(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.
|
||
|
||
An identifier that is already used in links cannot be modified,
|
||
otherwise all links will break.
|
||
|
||
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")
|
||
(define-obsolete-function-alias 'denote-rename-rename-keywords 'denote-rename-file-keywords "3.0.0")
|
||
|
||
(defun denote-rename-file-signature ()
|
||
"Convenience command to change the signature of a file.
|
||
Like `denote-rename-file', but prompts only for the signature.
|
||
|
||
Add or remove a signature in one go. Do this by prepopulating the
|
||
minibuffer prompt with the existing signature. The user can then modify
|
||
it accordingly. An empty input means to remove the signature
|
||
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 '(signature)))
|
||
(call-interactively #'denote-rename-file)))
|
||
|
||
(define-obsolete-function-alias 'denote-add-signature 'denote-rename-file-signature "3.0.0")
|
||
(define-obsolete-function-alias 'denote-remove-signature 'denote-rename-file-signature "3.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-dired-rename-files ()
|
||
"Rename Dired marked files same way as `denote-rename-file'.
|
||
Rename each file in sequence, making all the relevant prompts.
|
||
Unlike `denote-rename-file', do not prompt for confirmation of
|
||
the changes made to the file: perform them outright (same as
|
||
setting `denote-rename-confirmations' to a nil value)."
|
||
(declare (interactive-only t))
|
||
(interactive nil dired-mode)
|
||
(let ((denote-used-identifiers (denote--get-all-used-ids))
|
||
(denote-rename-confirmations nil))
|
||
(if-let* ((marks (dired-get-marked-files)))
|
||
(progn
|
||
(dolist (file marks)
|
||
(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 identifier)))
|
||
(denote-update-dired-buffers))
|
||
(user-error "No marked files; aborting"))))
|
||
|
||
(defalias 'denote-dired-rename-marked-files 'denote-dired-rename-files
|
||
"Alias for `denote-dired-rename-files'.")
|
||
|
||
(defun denote-keywords--combine (combination-type user-input-keywords keywords)
|
||
"COMBINATION-TYPE is either `:add', `:remove' or `:replace'.
|
||
|
||
USER-INPUT-KEYWORDS are new keywords collected from the end-user.
|
||
KEYWORDS are the existing keywords for the underlying file.
|
||
|
||
This function is an internal implementation function."
|
||
(cond
|
||
((eq combination-type :add)
|
||
(seq-union keywords user-input-keywords))
|
||
((eq combination-type :replace)
|
||
user-input-keywords)
|
||
((eq combination-type :remove)
|
||
(seq-difference keywords user-input-keywords))
|
||
(t
|
||
(error "Unknown operation in denote-keywords--combine: %s"
|
||
combination-type))))
|
||
|
||
(defun denote-dired-rename-marked-files--change-keywords (combination-type keywords-prompt)
|
||
"COMBINATION-TYPE is either `:add', `:remove' or `:replace'.
|
||
|
||
KEYWORDS-PROMPT is the prompt we show the end-user, when taking keywords
|
||
as input.
|
||
|
||
This function is an internal implementation function."
|
||
(if-let* ((marks (dired-get-marked-files)))
|
||
(let ((denote-prompts '())
|
||
(denote-rename-confirmations nil)
|
||
(user-input-keywords (denote-keywords-prompt keywords-prompt))
|
||
(denote-used-identifiers (denote--get-all-used-ids)))
|
||
(dolist (file marks)
|
||
(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 identifier)))
|
||
(denote-update-dired-buffers))
|
||
(user-error "No marked files; aborting")))
|
||
|
||
;;;###autoload
|
||
(defun denote-dired-rename-marked-files-with-keywords ()
|
||
"Rename marked files in Dired to a Denote file name by writing keywords.
|
||
|
||
Specifically, do the following:
|
||
|
||
- retain the file's existing name and make it the TITLE field,
|
||
per Denote's file-naming scheme;
|
||
|
||
- sluggify the TITLE, according to our conventions (check the
|
||
user option `denote-file-name-slug-functions');
|
||
|
||
- prepend an identifier to the TITLE;
|
||
|
||
- preserve the file's extension, if any;
|
||
|
||
- prompt once for KEYWORDS and apply the user's input to the
|
||
corresponding field in the file name, rewriting any keywords
|
||
that may exist while removing keywords that do exist if
|
||
KEYWORDS is empty;
|
||
|
||
- add or rewrite existing front matter to the underlying file, if it is
|
||
recognized as a Denote note (per the user option `denote-file-type'),
|
||
such that it includes the new keywords.
|
||
|
||
Construct the file name in accordance with the user option
|
||
`denote-file-name-components-order'.
|
||
|
||
Run the `denote-after-rename-file-hook' after renaming is done.
|
||
|
||
Also see the specialized commands to only add or remove keywords:
|
||
|
||
- `denote-dired-rename-marked-files-add-keywords'.
|
||
- `denote-dired-rename-marked-files-remove-keywords'."
|
||
(declare (interactive-only t))
|
||
(interactive nil dired-mode)
|
||
(denote-dired-rename-marked-files--change-keywords
|
||
:replace "Rename marked files with KEYWORDS, overwriting existing (empty to ignore/remove)"))
|
||
|
||
;;;###autoload
|
||
(defun denote-dired-rename-marked-files-add-keywords ()
|
||
"Like `denote-dired-rename-marked-files-with-keywords' to only add keywords."
|
||
(declare (interactive-only t))
|
||
(interactive nil dired-mode)
|
||
(denote-dired-rename-marked-files--change-keywords
|
||
:add "Add KEYWORDS to marked files"))
|
||
|
||
;;;###autoload
|
||
(defun denote-dired-rename-marked-files-remove-keywords ()
|
||
"Like `denote-dired-rename-marked-files-with-keywords' to only remove keywords."
|
||
(declare (interactive-only t))
|
||
(interactive nil dired-mode)
|
||
(denote-dired-rename-marked-files--change-keywords
|
||
:remove "Remove KEYWORDS from marked files"))
|
||
|
||
;;;###autoload
|
||
(defun denote-rename-file-using-front-matter (file)
|
||
"Rename FILE using its front matter as input.
|
||
When called interactively, FILE is the variable `buffer-file-name' or
|
||
the Dired file at point, which is subsequently inspected for the
|
||
requisite front matter. It is thus implied that the FILE has a file
|
||
type that is supported by Denote, per the user option `denote-file-type'.
|
||
|
||
The values of `denote-rename-confirmations',
|
||
`denote-save-buffers' and `denote-kill-buffers' are respected.
|
||
|
||
Only the front matter lines that appear in the front matter template (as
|
||
defined in `denote-file-types') will be handled.
|
||
|
||
To change the identifier (date) of the note with this command, the
|
||
identifier line (if present) of the front matter must be modified.
|
||
Modifying the date line has no effect.
|
||
|
||
While this command generally does not modify the front matter, there are
|
||
exceptions. The value of the `date' line will follow that of the
|
||
`identifier' line. If they are both in the front matter template and
|
||
the `date' line is missing, it will be added again. Similarly, if they
|
||
are both in the front matter template and the `date' line is present and
|
||
the `identifier' line has been removed, the `date' line will be removed
|
||
as well. Also, if the keywords are out of order and
|
||
`denote-sort-keywords' is non-nil, they will be sorted. There will be a
|
||
prompt for this if `denote-rename-confirmations' contains
|
||
`rewrite-front-matter'.
|
||
|
||
Construct the file name in accordance with the user option
|
||
`denote-file-name-components-order'."
|
||
(interactive (list (or (dired-get-filename nil t) buffer-file-name)))
|
||
(unless (denote-file-is-writable-and-supported-p file)
|
||
(user-error "The file is not writable or does not have a supported file extension"))
|
||
(let ((file-type (denote-filetype-heuristics file)))
|
||
(unless (denote--file-has-front-matter-p file file-type)
|
||
(user-error "The file does not appear to have a front matter"))
|
||
(let* ((front-matter-template (denote--front-matter file-type))
|
||
(components-in-template (denote--get-front-matter-components-order front-matter-template file-type))
|
||
(title (if (memq 'title components-in-template)
|
||
(or (denote-retrieve-front-matter-title-value file file-type) "")
|
||
(or (denote-retrieve-filename-title file) "")))
|
||
(keywords (if (memq 'keywords components-in-template)
|
||
(denote-retrieve-front-matter-keywords-value file file-type)
|
||
(denote-retrieve-filename-keywords-as-list file)))
|
||
(signature (if (memq 'signature components-in-template)
|
||
(or (denote-retrieve-front-matter-signature-value file file-type) "")
|
||
(or (denote-retrieve-filename-signature file) "")))
|
||
(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
|
||
(defun denote-dired-rename-marked-files-using-front-matter ()
|
||
"Call `denote-rename-file-using-front-matter' over the Dired marked files.
|
||
Refer to the documentation of that command for the technicalities.
|
||
|
||
Marked files must count as notes for the purposes of Denote, which means
|
||
that they at least have an identifier in their file name and use a
|
||
supported file type, per the user option `denote-file-type'. Files that
|
||
do not meet this criterion are ignored because Denote cannot know if
|
||
they have front matter and what that may be."
|
||
(interactive nil dired-mode)
|
||
(if-let* ((marks (seq-filter
|
||
(lambda (m)
|
||
(and (file-regular-p m)
|
||
(denote-file-is-writable-and-supported-p m)
|
||
(denote-file-has-identifier-p m)))
|
||
(dired-get-marked-files))))
|
||
(let ((denote-used-identifiers (denote--get-all-used-ids)))
|
||
(dolist (file marks)
|
||
(denote-rename-file-using-front-matter file))
|
||
(denote-update-dired-buffers))
|
||
(user-error "No marked Denote files; aborting")))
|
||
|
||
;;;;; Creation of front matter
|
||
|
||
(make-obsolete 'denote-add-front-matter nil "Use `denote-rename-file' or related. Starting with version 4.0.0.")
|
||
|
||
;;;###autoload
|
||
(defun denote-change-file-type-and-front-matter (file new-file-type)
|
||
"Change file type of FILE and add an appropriate front matter.
|
||
|
||
If in Dired, consider FILE to be the one at point, else the
|
||
current file, else prompt with minibuffer completion for one.
|
||
|
||
Add a front matter in the format of the NEW-FILE-TYPE at the
|
||
beginning of the file.
|
||
|
||
Retrieve the title of FILE from a line starting with a title
|
||
field in its front matter, depending on the previous file
|
||
type (e.g. #+title for Org). The same process applies for
|
||
keywords.
|
||
|
||
As a final step, ask for confirmation, showing the difference
|
||
between old and new file names.
|
||
|
||
Important note: No attempt is made to modify any other elements
|
||
of the file. This needs to be done manually.
|
||
|
||
Construct the file name in accordance with the user option
|
||
`denote-file-name-components-order'."
|
||
(interactive
|
||
(list
|
||
(denote--rename-dired-file-or-current-file-or-prompt)
|
||
(denote--valid-file-type (or (denote-file-type-prompt) denote-file-type))))
|
||
(let* ((initial-state (if (find-buffer-visiting file) 'visited 'not-visited))
|
||
(dir (file-name-directory file))
|
||
(old-file-type (denote-filetype-heuristics file))
|
||
(id (or (denote-retrieve-filename-identifier file) ""))
|
||
(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) ""))
|
||
(new-extension (denote--file-extension new-file-type))
|
||
(new-name (denote-format-file-name dir id keywords title new-extension signature))
|
||
(max-mini-window-height denote-rename-max-mini-window-height))
|
||
(when (denote-rename-file-prompt file new-name)
|
||
(denote-rename-file-and-buffer file new-name)
|
||
(denote-update-dired-buffers)
|
||
(when (and (denote-file-is-writable-and-supported-p new-name)
|
||
(denote-add-front-matter-prompt new-name))
|
||
(denote-prepend-front-matter new-name title keywords signature date id new-file-type)
|
||
(denote--handle-save-and-kill-buffer 'rename new-name initial-state)))))
|
||
|
||
;;;; The Denote faces
|
||
|
||
(defgroup denote-faces ()
|
||
"Faces for Denote."
|
||
:group 'denote)
|
||
|
||
(defface denote-faces-link '((t :inherit link))
|
||
"Face used to style Denote links in the buffer."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.5.0"))
|
||
|
||
(defface denote-faces-query-link '((t :inherit link-visited))
|
||
"Face used to style Denote query links in the buffer."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "4.0.0"))
|
||
|
||
(defface denote-faces-subdirectory '((t :inherit bold))
|
||
"Face for subdirectory of file name.
|
||
This should only ever needed in the backlinks' buffer (or
|
||
equivalent), not in Dired."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.2.0"))
|
||
|
||
(defface denote-faces-date '((t :inherit font-lock-variable-name-face))
|
||
"Face for file name date in Dired buffers.
|
||
This is the part of the identifier that covers the year, month,
|
||
and day."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.1.0"))
|
||
|
||
(defface denote-faces-time '((t :inherit denote-faces-date))
|
||
"Face for file name time in Dired buffers.
|
||
This is the part of the identifier that covers the hours, minutes,
|
||
and seconds."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.1.0"))
|
||
|
||
(defface denote-faces-title nil
|
||
"Face for file name title in Dired buffers."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.1.0"))
|
||
|
||
(defface denote-faces-year '((t :inherit denote-faces-date))
|
||
"Face for file name year in Dired buffers.
|
||
This is the part of the identifier that covers the year, month, and day."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.3.0"))
|
||
|
||
(defface denote-faces-month '((t :inherit denote-faces-date))
|
||
"Face for file name month in Dired buffers.
|
||
This is the part of the identifier that covers the year, month, and day."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.3.0"))
|
||
|
||
(defface denote-faces-day '((t :inherit denote-faces-date))
|
||
"Face for file name day in Dired buffers.
|
||
This is the part of the identifier that covers the year, month, and day."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.3.0"))
|
||
|
||
(defface denote-faces-hour '((t :inherit denote-faces-date))
|
||
"Face for file name hours in Dired buffers.
|
||
This is the part of the identifier that covers the hours, minutes,
|
||
and seconds."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.3.0"))
|
||
|
||
(defface denote-faces-minute '((t :inherit denote-faces-date))
|
||
"Face for file name minutes in Dired buffers.
|
||
This is the part of the identifier that covers the hours, minutes,
|
||
and seconds."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.3.0"))
|
||
|
||
(defface denote-faces-second '((t :inherit denote-faces-date))
|
||
"Face for file name seconds in Dired buffers.
|
||
This is the part of the identifier that covers the hours, minutes,
|
||
and seconds."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.3.0"))
|
||
|
||
(defface denote-faces-extension '((t :inherit shadow))
|
||
"Face for file extension type in Dired buffers."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.1.0"))
|
||
|
||
(defface denote-faces-keywords '((t :inherit font-lock-builtin-face))
|
||
"Face for file name keywords in Dired buffers."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.1.0"))
|
||
|
||
(defface denote-faces-signature '((t :inherit font-lock-warning-face))
|
||
"Face for file name signature in Dired buffers."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.0.0"))
|
||
|
||
(defface denote-faces-delimiter
|
||
'((((class color) (min-colors 88) (background light))
|
||
:foreground "gray70")
|
||
(((class color) (min-colors 88) (background dark))
|
||
:foreground "gray30")
|
||
(t :inherit shadow))
|
||
"Face for file name delimiters in Dired buffers."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "0.1.0"))
|
||
|
||
(defface denote-faces-time-delimiter '((t :inherit shadow))
|
||
"Face for the delimiter between date and time in Dired buffers."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.1.0"))
|
||
|
||
;; The following matchers must obey the doc of `font-lock-keywords':
|
||
;; - Have one parameter, the limit of the search
|
||
;; - Set match-data (and restore it on failure)
|
||
;; - Move point after the match (or restore it on failure).
|
||
;; - Return t on success and nil on failure. re-search-forward returns (point) on success. It may be better to do the same.
|
||
|
||
(defun denote-faces-dired-file-name-matcher (limit)
|
||
"Find the file name in a Dired line, not looking beyond LIMIT."
|
||
(let ((initial-match-data (match-data))
|
||
(initial-point (point))
|
||
(line-found nil))
|
||
;; Find the next non empty line that contains a Dired file name
|
||
(while (and (not line-found)
|
||
(re-search-forward "^.+$" limit t))
|
||
;; dired-move-to-filename moves the point even if it returns nil
|
||
(let ((saved-point (point)))
|
||
(if (and (dired-move-to-filename)
|
||
(save-match-data
|
||
(denote-file-has-denoted-filename-p (buffer-substring (point) (line-end-position)))))
|
||
(setq line-found t)
|
||
(goto-char saved-point))))
|
||
(if line-found
|
||
(let ((beginning-point (point)))
|
||
(goto-char (match-end 0))
|
||
(set-match-data (list beginning-point (match-end 0)))
|
||
(point))
|
||
(goto-char initial-point)
|
||
(set-match-data initial-match-data)
|
||
nil)))
|
||
|
||
(defun denote-faces-directory-matcher (limit)
|
||
"Match the directory in a Dired line, not looking beyond LIMIT."
|
||
(let ((initial-match-data (match-data))
|
||
(initial-point (point)))
|
||
(if (re-search-forward "\\(?1:.*/\\)[^/]*$" limit t)
|
||
(progn
|
||
(goto-char (match-end 1))
|
||
(set-match-data (list (match-beginning 1) (match-end 1)))
|
||
(point))
|
||
(goto-char initial-point)
|
||
(set-match-data initial-match-data)
|
||
nil)))
|
||
|
||
(defun denote-faces-signature-matcher (limit)
|
||
"Match the signature in a Dired line, not looking beyond LIMIT."
|
||
(let ((initial-match-data (match-data))
|
||
(initial-point (point)))
|
||
(if (or (re-search-forward "==\\(?1:[^/]*?\\)\\(@@\\|--\\|__\\|==\\|\\.\\)[^/]*$" limit t)
|
||
(re-search-forward "==\\(?1:[^/]*\\)$" limit t))
|
||
(progn
|
||
(goto-char (match-end 1))
|
||
(set-match-data (list (match-beginning 1) (match-end 1)))
|
||
(point))
|
||
(goto-char initial-point)
|
||
(set-match-data initial-match-data)
|
||
nil)))
|
||
|
||
(defun denote-faces-identifier-matcher (limit)
|
||
"Match a general identifier in a Dired line, not looking beyond LIMIT."
|
||
(let ((initial-match-data (match-data))
|
||
(initial-point (point)))
|
||
(if (or (re-search-forward "@@\\(?1:[^/]*?\\)\\(@@\\|--\\|__\\|==\\|\\.\\)[^/]*$" limit t)
|
||
(re-search-forward "@@\\(?1:[^/]*\\)$" limit t))
|
||
(progn
|
||
(goto-char (match-end 1))
|
||
(set-match-data (list (match-beginning 1) (match-end 1)))
|
||
(point))
|
||
(goto-char initial-point)
|
||
(set-match-data initial-match-data)
|
||
nil)))
|
||
|
||
(defun denote-faces-title-matcher (limit)
|
||
"Match the title in a Dired line, not looking beyond LIMIT."
|
||
(let ((initial-match-data (match-data))
|
||
(initial-point (point)))
|
||
(if (or (re-search-forward "--\\(?1:[^/]*?\\)\\(@@\\|__\\|==\\|\\.\\)[^/]*$" limit t)
|
||
(re-search-forward "--\\(?1:[^/]*\\)$" limit t))
|
||
(progn
|
||
(goto-char (match-end 1))
|
||
(set-match-data (list (match-beginning 1) (match-end 1)))
|
||
(point))
|
||
(goto-char initial-point)
|
||
(set-match-data initial-match-data)
|
||
nil)))
|
||
|
||
(defun denote-faces-keywords-matcher (limit)
|
||
"Match the keywords in a Dired line, not looking beyond LIMIT."
|
||
(let ((initial-match-data (match-data))
|
||
(initial-point (point)))
|
||
(if (or (re-search-forward "__\\(?1:[^/]*?\\)\\(@@\\|--\\|__\\|==\\|\\.\\)[^/]*$" limit t)
|
||
(re-search-forward "__\\(?1:[^/]*\\)$" limit t))
|
||
(progn
|
||
(goto-char (match-end 1))
|
||
(set-match-data (list (match-beginning 1) (match-end 1)))
|
||
(point))
|
||
(goto-char initial-point)
|
||
(set-match-data initial-match-data)
|
||
nil)))
|
||
|
||
(defconst denote-faces-matchers
|
||
`((denote-faces-directory-matcher
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-subdirectory nil t))
|
||
;; Identifier with format 00000000T000000
|
||
("\\(?1:[0-9]\\{4\\}\\)\\(?2:[0-9]\\{2\\}\\)\\(?3:[0-9]\\{2\\}\\)\\(?7:T\\)\\(?4:[0-9]\\{2\\}\\)\\(?5:[0-9]\\{2\\}\\)\\(?6:[0-9]\\{2\\}\\)"
|
||
(goto-char (match-beginning 0)) ; pre-form, executed before looking for the first identifier
|
||
(goto-char (match-end 0)) ; post-form, executed after all matches (identifiers here) are found
|
||
(1 'denote-faces-year nil t)
|
||
(2 'denote-faces-month nil t)
|
||
(3 'denote-faces-day nil t)
|
||
(4 'denote-faces-hour nil t)
|
||
(5 'denote-faces-minute nil t)
|
||
(6 'denote-faces-second nil t)
|
||
(7 'denote-faces-delimiter nil t))
|
||
;; Identifier with general format (not yet possible)
|
||
(denote-faces-identifier-matcher
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-date nil t))
|
||
;; Title
|
||
(denote-faces-title-matcher
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-title nil t))
|
||
;; Keywords
|
||
(denote-faces-keywords-matcher
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-keywords nil t))
|
||
;; Signature
|
||
(denote-faces-signature-matcher
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-signature nil t))
|
||
;; Delimiters
|
||
("\\(@@\\|--\\|__\\|==\\)"
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-delimiter nil t))
|
||
;; Extension
|
||
("\\..*$"
|
||
(goto-char (match-beginning 0))
|
||
(goto-char (match-end 0))
|
||
(0 'denote-faces-extension nil t)))
|
||
"Matchers for fontification of file names.")
|
||
|
||
(defconst denote-faces-file-name-keywords-for-dired
|
||
`((denote-faces-dired-file-name-matcher ,@denote-faces-matchers))
|
||
"Keywords for fontification of file names.")
|
||
|
||
(make-obsolete-variable 'denote-faces-file-name-keywords-for-backlinks nil "4.0.0")
|
||
|
||
(defface denote-faces-prompt-old-name '((t :inherit error))
|
||
"Face for the old name shown in the prompt of `denote-rename-file' etc."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.2.0"))
|
||
|
||
(defface denote-faces-prompt-new-name '((t :inherit success))
|
||
"Face for the new name shown in the prompt of `denote-rename-file' etc."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.2.0"))
|
||
|
||
(defface denote-faces-prompt-current-name '((t :inherit denote-faces-prompt-old-name))
|
||
"Face for the current file shown in the prompt of `denote-rename-file' etc."
|
||
:group 'denote-faces
|
||
:package-version '(denote . "2.2.0"))
|
||
|
||
;;;; Fontification in Dired
|
||
|
||
(defgroup denote-dired ()
|
||
"Integration between Denote and Dired."
|
||
:group 'denote)
|
||
|
||
(defcustom denote-dired-directories (if (listp denote-directory)
|
||
denote-directory
|
||
(list denote-directory))
|
||
"List of directories where `denote-dired-mode' should apply to.
|
||
For this to take effect, add `denote-dired-mode-in-directories',
|
||
to the `dired-mode-hook'.
|
||
|
||
If `denote-dired-directories-include-subdirectories' is non-nil,
|
||
also apply the effect to all subdirectories of those specified in
|
||
the list."
|
||
:type '(repeat directory)
|
||
:package-version '(denote . "0.1.0")
|
||
:link '(info-link "(denote) Fontification in Dired")
|
||
:group 'denote-dired)
|
||
|
||
(defcustom denote-dired-directories-include-subdirectories nil
|
||
"If non-nil `denote-dired-directories' also affects all subdirectories.
|
||
Otherwise `denote-dired-directories' works only with exact matches."
|
||
:package-version '(denote . "2.2.0")
|
||
:link '(info-link "(denote) Fontification in Dired")
|
||
:type 'boolean
|
||
:group 'denote-dired)
|
||
|
||
;; FIXME 2022-08-12: Make `denote-dired-mode' work with diredfl. This
|
||
;; may prove challenging.
|
||
|
||
(defun denote-dired-add-font-lock (&rest _)
|
||
"Append `denote-faces-file-name-keywords' to font lock keywords."
|
||
;; NOTE 2023-10-28: I tried to add the first argument and then
|
||
;; experimented with various combinations of keywords, such as
|
||
;; `(,@dired-font-lock-keywords ,@denote-faces-file-name-keywords).
|
||
;; None of them could be unset upon disabling `denote-dired-mode'.
|
||
;; As such, I am using the `when' here.
|
||
(when (derived-mode-p 'dired-mode)
|
||
(font-lock-add-keywords nil denote-faces-file-name-keywords-for-dired t)))
|
||
|
||
(defun denote-dired-remove-font-lock (&rest _)
|
||
"Remove `denote-faces-file-name-keywords' from font lock keywords."
|
||
;; See NOTE in `denote-dired-add-font-lock'.
|
||
(when (derived-mode-p 'dired-mode)
|
||
(font-lock-remove-keywords nil denote-faces-file-name-keywords-for-dired)))
|
||
|
||
(declare-function wdired-change-to-wdired-mode "wdired")
|
||
(declare-function wdired-finish-edit "wdired")
|
||
|
||
;;;###autoload
|
||
(define-minor-mode denote-dired-mode
|
||
"Fontify all Denote-style file names.
|
||
Add this or `denote-dired-mode-in-directories' to
|
||
`dired-mode-hook'."
|
||
:global nil
|
||
:group 'denote-dired
|
||
(if denote-dired-mode
|
||
(progn
|
||
(denote-dired-add-font-lock)
|
||
(advice-add #'wdired-change-to-wdired-mode :after #'denote-dired-add-font-lock)
|
||
(advice-add #'wdired-finish-edit :after #'denote-dired-add-font-lock))
|
||
(denote-dired-remove-font-lock)
|
||
(advice-remove #'wdired-change-to-wdired-mode #'denote-dired-add-font-lock)
|
||
(advice-remove #'wdired-finish-edit #'denote-dired-add-font-lock))
|
||
(font-lock-flush (point-min) (point-max)))
|
||
|
||
(defun denote-dired--modes-dirs-as-dirs ()
|
||
"Return `denote-dired-directories' as directories.
|
||
The intent is to basically make sure that however a path is
|
||
written, it is always returned as a directory."
|
||
(mapcar
|
||
(lambda (dir)
|
||
(file-name-as-directory (file-truename dir)))
|
||
denote-dired-directories))
|
||
|
||
;;;###autoload
|
||
(defun denote-dired-mode-in-directories ()
|
||
"Enable `denote-dired-mode' in `denote-dired-directories'.
|
||
Add this function to `dired-mode-hook'.
|
||
|
||
If `denote-dired-directories-include-subdirectories' is non-nil,
|
||
also enable it in all subdirectories."
|
||
(when-let* ((dirs (denote-dired--modes-dirs-as-dirs))
|
||
;; Also include subdirs
|
||
((or (member (file-truename default-directory) dirs)
|
||
(and denote-dired-directories-include-subdirectories
|
||
(seq-some
|
||
(lambda (dir)
|
||
(string-prefix-p dir (file-truename default-directory)))
|
||
dirs)))))
|
||
(denote-dired-mode 1)))
|
||
|
||
;;;; The linking facility
|
||
|
||
;;;;; Link to note
|
||
|
||
(defvar denote-org-link-format "[[denote:%s][%s]]"
|
||
"Format of Org link to note.
|
||
The value is passed to `format' with IDENTIFIER and TITLE
|
||
arguments, in this order.
|
||
|
||
Also see `denote-org-link-in-context-regexp'.")
|
||
|
||
(defvar denote-md-link-format "[%2$s](denote:%1$s)"
|
||
"Format of Markdown link to note.
|
||
The %N$s notation used in the default value is for `format' as
|
||
the supplied arguments are IDENTIFIER and TITLE, in this order.
|
||
|
||
Also see `denote-md-link-in-context-regexp'.")
|
||
|
||
(defvar denote-id-only-link-format "[[denote:%s]]"
|
||
"Format of identifier-only link to note.
|
||
The value is passed to `format' with IDENTIFIER as its sole
|
||
argument.
|
||
|
||
Also see `denote-id-only-link-in-context-regexp'.")
|
||
|
||
(defvar denote-org-link-in-context-regexp
|
||
(concat "\\[\\[" "denote:"
|
||
"\\(?1:[^][]*?\\)"
|
||
"\\(?:::.*\\)?" "]"
|
||
"\\[" "\\(?2:" ".*?" "\\)" "]]")
|
||
"Regexp to match an Org link in its context.
|
||
The format of such links is `denote-org-link-format'.")
|
||
|
||
(defvar denote-md-link-in-context-regexp
|
||
(concat "\\[" "\\(?2:" ".*?" "\\)" "]"
|
||
"(denote:" "\\(?1:[^][]*?\\)" ")")
|
||
"Regexp to match a Markdown link in its context.
|
||
The format of such links is `denote-md-link-format'.")
|
||
|
||
(defvar denote-id-only-link-in-context-regexp
|
||
(concat "\\[\\[" "denote:" "\\(?1:[^][]*?\\)" "]]")
|
||
"Regexp to match an identifier-only link in its context.
|
||
The format of such links is `denote-id-only-link-format'.")
|
||
|
||
(defun denote-format-link (file description file-type id-only)
|
||
"Prepare link to FILE using DESCRIPTION.
|
||
|
||
FILE-TYPE and ID-ONLY are used to get the format of the link.
|
||
See the `:link' property of `denote-file-types'."
|
||
(format
|
||
(cond
|
||
((or id-only (null description) (string-empty-p description))
|
||
denote-id-only-link-format)
|
||
;; NOTE 2024-05-20: If there is no file type, we want to use the
|
||
;; Org format because it is still a usable link with the help of
|
||
;; the command `org-open-at-point-global'.
|
||
((null file-type)
|
||
(denote--link-format 'org))
|
||
(t
|
||
(denote--link-format file-type)))
|
||
(denote-retrieve-filename-identifier file)
|
||
description))
|
||
|
||
;; NOTE 2025-11-23: The only reason I have &optional is because I want
|
||
;; it to be backward compatible. We used to have a single FILE
|
||
;; parameter.
|
||
(defun denote-link-description-with-signature-and-title (file &optional file-type)
|
||
"Return link description for FILE with FILE-TYPE.
|
||
For backward compatibility, FILE-TYPE is an optional parameter. If it
|
||
is nil, then compute FILE-TYPE internally.
|
||
|
||
- If the region is active, use it as the description.
|
||
|
||
- If FILE has a signature, then format the description as a sequence of
|
||
the signature text and the title with two spaces between them.
|
||
|
||
- If FILE does not have a signature, then use its title as the
|
||
description.
|
||
|
||
- If none of the above works, return an empty string.
|
||
|
||
This function is useful as the value of the user option
|
||
`denote-link-description-format' (which can optionally be bound to a
|
||
function)."
|
||
(let* ((type (or file-type (denote-filetype-heuristics file)))
|
||
(signature (denote-retrieve-filename-signature file))
|
||
(title (denote-retrieve-title-or-filename file type))
|
||
(region-text (denote--get-active-region-content)))
|
||
(cond
|
||
(region-text region-text)
|
||
((and signature title) (format "%s %s" signature title))
|
||
(title (format "%s" title))
|
||
(signature (format "%s" signature))
|
||
(t ""))))
|
||
|
||
(defun denote--get-active-region-content ()
|
||
"Return the text of the active region, else nil."
|
||
(when-let* ((_ (region-active-p))
|
||
(beg (region-beginning))
|
||
(end (region-end))
|
||
(contents (buffer-substring-no-properties beg end))
|
||
(_ (not (string-blank-p contents))))
|
||
(string-trim contents)))
|
||
|
||
(defun denote--delete-active-region-content ()
|
||
"Delete the content of the active region, if any."
|
||
(when-let* ((_ (region-active-p))
|
||
(beg (region-beginning))
|
||
(end (region-end)))
|
||
(delete-region beg end)))
|
||
|
||
(defun denote-get-link-description (file &optional file-type)
|
||
"Return a link description for FILE.
|
||
|
||
If `denote-link-description-format' is a function, call it with FILE and
|
||
FILE-TYPE as argument. It should return a string, representing the link
|
||
description.
|
||
|
||
If the user option `denote-link-description-format' is a string, parse
|
||
it to substitute any format specifiers therein with their respective
|
||
values (see the documentation of that user option). If the region is
|
||
active, use it as the description.
|
||
|
||
For backward compatibility, support the scenario where the function
|
||
assigned to `denote-link-description-format' accepts a single FILE
|
||
argument. In that case, the function takes care to find the TYPE on its
|
||
own to return the appropriate description."
|
||
(cond
|
||
((functionp denote-link-description-format)
|
||
;; NOTE 2025-11-23: `denote-link-description-format' used to
|
||
;; accept a function with a single parameter. Now we expect two
|
||
;; arguments, but must be backward compatible.
|
||
(if (> (cdr (func-arity denote-link-description-format)) 1)
|
||
(funcall denote-link-description-format file file-type)
|
||
(display-warning 'denote "The `denote-link-description-format' function is now called with FILE and FILE-TYPE" :warning)
|
||
(funcall denote-link-description-format file)))
|
||
((stringp denote-link-description-format)
|
||
(if-let* ((region (denote--get-active-region-content)))
|
||
region
|
||
(let ((type (or file-type (denote-filetype-heuristics file))))
|
||
(string-trim
|
||
(format-spec denote-link-description-format
|
||
(list (cons ?t (cond
|
||
((denote-retrieve-front-matter-title-value file (denote-filetype-heuristics file)))
|
||
((denote-retrieve-filename-title file))
|
||
(t "")))
|
||
(cons ?T (or (denote-retrieve-filename-title file) ""))
|
||
(cons ?i (or (denote-retrieve-filename-identifier file) ""))
|
||
;; 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-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-date-identifier-p id)
|
||
(format-time-string "%A, %e %B %Y" (date-to-time (denote-id-to-date id)))
|
||
id)))
|
||
(t "")))
|
||
(cons ?d (or (denote-retrieve-filename-identifier file) ""))
|
||
(cons ?s (or (denote-retrieve-filename-signature file) ""))
|
||
(cons ?k (or (denote-retrieve-filename-keywords file) ""))
|
||
(cons ?% "%"))
|
||
'delete)))))
|
||
(t
|
||
(error "The `denote-link-description-format' must be a function or string"))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--link-get-description
|
||
'denote-get-link-description
|
||
"4.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-link (file file-type description &optional id-only)
|
||
"Create link to FILE note in variable `denote-directory' with DESCRIPTION.
|
||
|
||
When called interactively, prompt for FILE using completion. In this
|
||
case, derive FILE-TYPE from the current buffer. FILE-TYPE is used to
|
||
determine the format of the link.
|
||
|
||
Return the DESCRIPTION of the link in the format specified by
|
||
`denote-link-description-format'. The default is to return the text of
|
||
the active region or the title of the note (plus the signature if
|
||
present).
|
||
|
||
With optional ID-ONLY as a non-nil argument, such as with a universal
|
||
prefix (\\[universal-argument]), insert links with just the identifier
|
||
and no further description. In this case, the link format is always
|
||
[[denote:IDENTIFIER]].
|
||
|
||
If the DESCRIPTION is empty, format the link the same as with ID-ONLY.
|
||
|
||
When called from Lisp, FILE is a string representing a full file system
|
||
path. FILE-TYPE is a symbol as described in the user option
|
||
`denote-file-type'. DESCRIPTION is a string. Whether the caller treats
|
||
the active region specially, is up to it."
|
||
(interactive
|
||
(let* ((file (denote-file-prompt nil "Link to FILE" nil :has-identifier))
|
||
(file-type (denote-filetype-heuristics buffer-file-name))
|
||
(description (when (file-exists-p file)
|
||
(denote-get-link-description file))))
|
||
(list file file-type description current-prefix-arg)))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(unless (file-exists-p file)
|
||
(user-error "The linked file does not exist"))
|
||
(denote--delete-active-region-content)
|
||
(insert (denote-format-link file description file-type id-only)))
|
||
|
||
(defalias 'denote-insert-link 'denote-link
|
||
"Alias for `denote-link' command.")
|
||
|
||
(make-obsolete 'denote-link-with-signature nil " 4.0.0: Use the `denote-link-description-format'.")
|
||
|
||
(defun denote-link--collect-identifiers (regexp)
|
||
"Return collection of identifiers in buffer matching REGEXP."
|
||
(let (matches)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(while (re-search-forward regexp nil t)
|
||
(push (match-string-no-properties 1) matches)))
|
||
matches))
|
||
|
||
(make-obsolete 'denote-link--expand-identifiers nil "4.1.0")
|
||
|
||
(defvar denote-link-find-file-history nil
|
||
"History for `denote-find-link'.")
|
||
|
||
(defalias 'denote-link--find-file-history 'denote-link-find-file-history
|
||
"Compatibility alias for `denote-link-find-file-history'.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-select-linked-file-prompt
|
||
'denote-select-from-files-prompt
|
||
"4.1.0")
|
||
|
||
(defun denote-select-from-files-prompt (files &optional prompt-text)
|
||
"Prompt for linked file among FILES.
|
||
Show relative file names and then return the absolute version of the
|
||
selected one.
|
||
|
||
With optional PROMPT-TEXT use it for the minibuffer prompt instead of
|
||
the generic one."
|
||
(let* ((roots (denote-directories))
|
||
(single-dir-p (null (cdr roots)))
|
||
(file-names (if single-dir-p
|
||
(mapcar
|
||
(lambda (file)
|
||
(denote--get-file-name-relative-to-directories file roots))
|
||
files)
|
||
files))
|
||
(selected (completing-read
|
||
(format-prompt (or prompt-text "Select file among files") nil)
|
||
(apply 'denote-get-completion-table file-names denote-file-prompt-extra-metadata)
|
||
nil t nil 'denote-link-find-file-history)))
|
||
(if single-dir-p
|
||
(expand-file-name selected (car roots))
|
||
selected)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-return-links
|
||
'denote-get-links
|
||
"4.1.0")
|
||
|
||
(defun denote-get-links (&optional file files)
|
||
"Return list of links in current or optional FILE.
|
||
With optional FILES, consider only those, otherwise use the return value
|
||
of `denote-directory-files'.
|
||
|
||
Also see `denote-get-backlinks'."
|
||
(when-let* ((current-file (or file (buffer-file-name)))
|
||
((denote-file-has-supported-extension-p current-file))
|
||
(file-type (denote-filetype-heuristics current-file))
|
||
(regexp (denote--link-in-context-regexp file-type))
|
||
(files (or files (denote-directory-files nil nil nil nil :has-identifier)))
|
||
(file-identifiers
|
||
(with-temp-buffer
|
||
(insert-file-contents current-file)
|
||
(denote-link--collect-identifiers regexp)))
|
||
(file-identifiers-hash-table (make-hash-table :test #'equal)))
|
||
(dolist (id file-identifiers)
|
||
(puthash id t file-identifiers-hash-table))
|
||
(let ((found-files))
|
||
(dolist (file files)
|
||
(when (gethash (denote-retrieve-filename-identifier file) file-identifiers-hash-table)
|
||
(push file found-files)))
|
||
found-files)))
|
||
|
||
;;;###autoload
|
||
(defun denote-find-link ()
|
||
"Use minibuffer completion to visit linked file.
|
||
Also see `denote-find-backlink'."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(when-let* ((links (or (denote-get-links)
|
||
(user-error "No links found")))
|
||
(selected (denote-select-from-files-prompt links "Select among LINKS")))
|
||
(find-file selected)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-after-creating (&optional id-only)
|
||
"Create new note in the background and link to it directly.
|
||
|
||
Use `denote' interactively to produce the new note. Its doc
|
||
string explains which prompts will be used and under what
|
||
conditions.
|
||
|
||
With optional ID-ONLY as a prefix argument create a link that
|
||
consists of just the identifier. Else try to also include the
|
||
file's title. This has the same meaning as in `denote-link'.
|
||
|
||
For a variant of this, see `denote-link-after-creating-with-command'.
|
||
|
||
IMPORTANT NOTE: Normally, `denote' does not save the buffer it
|
||
produces for the new note. This is a safety precaution to not
|
||
write to disk unless the user wants it (e.g. the user may choose
|
||
to kill the buffer, thus cancelling the creation of the note).
|
||
However, for this command the creation of the note happens in the
|
||
background and the user may miss the step of saving their buffer.
|
||
We thus have to save the buffer in order to (i) establish valid
|
||
links, and (ii) retrieve whatever front matter from the target
|
||
file. Though see `denote-save-buffer-after-creation'."
|
||
(interactive "P")
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(let* ((type (denote-filetype-heuristics (buffer-file-name)))
|
||
(path (denote--command-with-features #'denote nil nil :save :in-background))
|
||
(description (denote-get-link-description path)))
|
||
(denote-link path type description id-only)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-after-creating-with-command (command &optional id-only)
|
||
"Like `denote-link-after-creating' but prompt for note-making COMMAND.
|
||
Use this to, for example, call `denote-signature' so that the
|
||
newly created note has a signature as part of its file name.
|
||
|
||
Optional ID-ONLY has the same meaning as in the command
|
||
`denote-link-after-creating'."
|
||
(interactive
|
||
(list
|
||
(denote-command-prompt)
|
||
current-prefix-arg))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(let* ((type (denote-filetype-heuristics (buffer-file-name)))
|
||
(path (denote--command-with-features command nil nil :save :in-background))
|
||
(description (denote-get-link-description path)))
|
||
(denote-link path type description id-only)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-or-create (target &optional id-only)
|
||
"Use `denote-link' on TARGET file, creating it if necessary.
|
||
|
||
If TARGET file does not exist, call `denote-link-after-creating' which
|
||
runs the `denote' command interactively to create the file. The
|
||
established link will then be targeting that new file. In that case,
|
||
use the last input at the file prompt as the default value of the title
|
||
prompt.
|
||
|
||
With optional ID-ONLY as a prefix argument create a link that
|
||
consists of just the identifier. Else try to also include the
|
||
file's title. This has the same meaning as in `denote-link'."
|
||
(interactive
|
||
(let* ((target (denote-file-prompt nil "Select file (RET on no match to create it)" :no-require-match :has-identifier)))
|
||
(unless (and target (file-exists-p target))
|
||
(setq target (denote--command-with-features #'denote :use-file-prompt-as-def-title :ignore-region :save :in-background)))
|
||
(list target current-prefix-arg)))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(denote-link target
|
||
(denote-filetype-heuristics (buffer-file-name))
|
||
(denote-get-link-description target)
|
||
id-only))
|
||
|
||
(defalias 'denote-link-to-existing-or-new-note 'denote-link-or-create
|
||
"Alias for `denote-link-or-create' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-link-or-create-with-command (&optional id-only)
|
||
"Like `denote-link-or-create' but prompt for a note-making command.
|
||
Use this to, for example, call `denote-signature' when the target file
|
||
does not exist, so that the newly created note has a signature as part
|
||
of its file name.
|
||
|
||
Optional ID-ONLY has the same meaning as in the command
|
||
`denote-link-or-create'."
|
||
(declare (interactive-only t))
|
||
(interactive "P")
|
||
(let ((target (denote-file-prompt nil "Select file (RET on no match to create it)" :no-require-match :has-identifier)))
|
||
(unless (file-exists-p target)
|
||
(setq target (denote--command-with-features (denote-command-prompt) :use-file-prompt-as-def-title :ignore-region :save :in-background)))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name
|
||
(denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(denote-link target
|
||
(denote-filetype-heuristics (buffer-file-name))
|
||
(denote-get-link-description target)
|
||
id-only)))
|
||
|
||
;;;;; Links' buffer (query links and backlinks using `denote-query-mode')
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-backlinks-mode
|
||
'denote-query-mode
|
||
"4.0.0")
|
||
|
||
(declare-function outline-cycle "outline" (&optional event))
|
||
(declare-function outline-cycle-buffer "outline" (&optional level))
|
||
(declare-function outline-next-heading "outline" ())
|
||
(declare-function outline-previous-heading "outline" ())
|
||
|
||
(defvar denote-query-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map "a" #'outline-cycle-buffer)
|
||
(define-key map "f" #'denote-query-focus-last-search)
|
||
(define-key map "i" #'denote-query-only-include-files)
|
||
(define-key map "I" #'denote-query-only-include-files-with-keywords)
|
||
(define-key map "j" #'outline-next-heading)
|
||
(define-key map "k" #'outline-previous-heading)
|
||
(define-key map "X" #'denote-query-exclude-files-with-keywords)
|
||
(define-key map "l" #'recenter-current-error)
|
||
(define-key map "o" #'delete-other-windows)
|
||
(define-key map "s" #'denote-grep)
|
||
(define-key map "S" #'denote-query-sort-last-search)
|
||
(define-key map "v" #'outline-cycle)
|
||
(define-key map "x" #'denote-query-exclude-files)
|
||
(define-key map "G" #'denote-query-clear-all-filters)
|
||
map)
|
||
"Keymap for `denote-query-mode' buffers.")
|
||
|
||
(define-derived-mode denote-query-mode xref--xref-buffer-mode "Denote Query"
|
||
"Major mode for queries found in the variable `denote-directory'.
|
||
This is used by the commands `denote-backlinks', `denote-grep',
|
||
`denote-query-contents-link', among others."
|
||
:interactive nil
|
||
(setq-local outline-minor-mode-use-buttons 'in-margins)
|
||
(outline-minor-mode 1))
|
||
|
||
(make-obsolete 'denote-link--backlink-find-file nil "4.0.0")
|
||
(make-obsolete 'denote-link--display-buffer nil "4.0.0")
|
||
(make-obsolete 'denote-backlinks-mode-next nil "4.0.0")
|
||
(make-obsolete 'denote-backlinks-mode-previous nil "4.0.0")
|
||
(make-obsolete 'denote-backlinks-toggle-context nil "4.0.0")
|
||
(make-obsolete-variable 'denote-backlinks-mode-map nil "4.0.0")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link--prepare-backlinks
|
||
'denote-make-links-buffer
|
||
"4.0.0")
|
||
|
||
(make-obsolete-variable 'denote-backlinks-show-context nil "4.0.0")
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-link-backlinks-display-buffer-action
|
||
'denote-backlinks-display-buffer-action
|
||
"3.1.0")
|
||
|
||
(defgroup denote-query ()
|
||
"Integration between Denote and Xref for grep/query/backlink buffers."
|
||
:group 'denote)
|
||
|
||
(defcustom denote-query-sorting nil
|
||
"How to sort files in query buffers.
|
||
This applies to buffers generated by the `denote-grep' command, query
|
||
links for file contents, as well as backlinks.
|
||
|
||
By default (a nil value), no sorting of matching files is performed.
|
||
Files are displayed in the order they are returned by the search
|
||
program (which often, but not always, means they are sorted by file
|
||
name).
|
||
|
||
The value can be a symbol among those listed in the value of the
|
||
variable `denote-sort-components'. Files will then be sorted according
|
||
to the given file name component or the relevant method (e.g. randomly
|
||
for the `random' symbol).
|
||
|
||
The value may also be a custom function. It takes two arguments, as
|
||
described by `sort' (internally, the function responsible is the
|
||
`denote-sort-files').
|
||
|
||
Whatever the value of `denote-query-sorting', users may change the
|
||
sorting of query buffers on demand by calling the command
|
||
`denote-query-sort-last-search'."
|
||
:type '(radio
|
||
(const :tag "Don't sort (default)" nil)
|
||
(const :tag "Sort by identifier" identifier)
|
||
(const :tag "Sort by title" title)
|
||
(const :tag "Sort by keywords" keywords)
|
||
(const :tag "Sort by signature" signature)
|
||
(const :tag "Random order" random)
|
||
(const :tag "Last modified" last-modified)
|
||
(function :tag "Custom function that take two arguments for `sort'"))
|
||
:package-version '(denote . "4.1.0")
|
||
:group 'denote-query)
|
||
|
||
(defcustom denote-backlinks-display-buffer-action
|
||
'((display-buffer-reuse-mode-window display-buffer-below-selected)
|
||
(mode . denote-query-mode)
|
||
(dedicated . t)
|
||
(preserve-size . (t . t))
|
||
(window-height . fit-window-to-buffer)
|
||
(body-function . select-window))
|
||
"The action used to display the current file's backlinks buffer.
|
||
|
||
The value has the form (FUNCTION . ALIST), where FUNCTION is
|
||
either an \"action function\", a list thereof, or possibly an
|
||
empty list. ALIST is a list of \"action alist\" which may be
|
||
omitted (or be empty).
|
||
|
||
Sample configuration to display the buffer in a side window on
|
||
the left of the Emacs frame:
|
||
|
||
(setq denote-backlinks-display-buffer-action
|
||
(quote ((display-buffer-reuse-window display-buffer-in-side-window)
|
||
(side . left)
|
||
(slot . 99)
|
||
(window-width . 0.3)
|
||
(dedicated . t)
|
||
(preserve-size . (t . t)))))
|
||
|
||
See Info node `(elisp) Displaying Buffers' for more details
|
||
and/or the documentation string of `display-buffer'."
|
||
:risky t
|
||
:type `(choice
|
||
(alist :key-type
|
||
(choice :tag "Condition"
|
||
regexp
|
||
(function :tag "Matcher function"))
|
||
:value-type ,display-buffer--action-custom-type)
|
||
(function :tag "Custom function to return an action alist"))
|
||
:package-version '(denote . "4.2.0")
|
||
:group 'denote-query)
|
||
|
||
(defcustom denote-query-links-display-buffer-action
|
||
'((display-buffer-reuse-mode-window display-buffer-below-selected)
|
||
(mode . (denote-query-mode dired))
|
||
(dedicated . t)
|
||
(preserve-size . (t . t))
|
||
(window-height . fit-window-to-buffer)
|
||
(body-function . select-window))
|
||
"The action used to display query links.
|
||
This is the same as `denote-backlinks-display-buffer-action'. Refer to
|
||
its documentation for the technicalities."
|
||
:risky t
|
||
:type `(choice
|
||
(alist :key-type
|
||
(choice :tag "Condition"
|
||
regexp
|
||
(function :tag "Matcher function"))
|
||
:value-type ,display-buffer--action-custom-type)
|
||
(function :tag "Custom function to return an action alist"))
|
||
:package-version '(denote . "4.2.0")
|
||
:group 'denote-query)
|
||
|
||
(defcustom denote-query-format-heading-function #'denote-get-file-name-relative-to-denote-directory
|
||
"Function used to construct headings for files matched by a query.
|
||
|
||
It is called with a single argument, the path to the note file, and it
|
||
should always return a string.
|
||
|
||
This is used in the buffer that shows backlinks, query links for file
|
||
contents, or the results of `denote-grep'."
|
||
:package-version '(denote . "4.2.0")
|
||
:link '(info-link "(denote) Use denote-grep to search inside files")
|
||
:group 'denote-query
|
||
:type 'function)
|
||
|
||
(defcustom denote-query-untitled-string "[Untitled]"
|
||
"String to use as heading for untitled notes in links' buffer.
|
||
|
||
Used only by `denote-query-extract-title'."
|
||
:package-version '(denote . "4.0.0")
|
||
:link '(info-link "(denote) Use denote-grep to search inside files")
|
||
:group 'denote-query
|
||
:type 'string)
|
||
|
||
(defun denote-query-extract-title (file)
|
||
"Extract note title from FILE front matter.
|
||
|
||
When no title is found, return title found in FILE name.
|
||
|
||
When that doesn't work, return `denote-query-untitled-string'.
|
||
|
||
Intended to be used as `denote-query-format-heading-function'."
|
||
(if-let* ((type (denote-filetype-heuristics file))
|
||
(title (denote-retrieve-title-or-filename file type))
|
||
(_ (not (string-blank-p title))))
|
||
title
|
||
denote-query-untitled-string))
|
||
|
||
(defun denote--display-buffer-from-xref-alist (xref-alist buffer-name display-buffer-action)
|
||
"Create buffer called BUFFER-NAME for XREF-ALIST.
|
||
|
||
DISPLAY-BUFFER-ACTION is a `display-buffer' action and concomitant
|
||
alist, such as `denote-backlinks-display-buffer-action'."
|
||
(let* ((inhibit-read-only t)
|
||
(file buffer-file-name)
|
||
(dirs (denote-directories)))
|
||
(unless xref-alist
|
||
(error "No results to display"))
|
||
;; Update group of each item in xref-alist
|
||
(dolist (x xref-alist)
|
||
(let* ((file-xref (car x)))
|
||
(setf (car x) (funcall denote-query-format-heading-function file-xref))))
|
||
(with-current-buffer (get-buffer-create buffer-name)
|
||
(erase-buffer)
|
||
(denote-query-mode)
|
||
;; In the links' buffer, the values of variables set in a
|
||
;; `.dir-locals.el` do not apply. We need to set
|
||
;; `denote-directory' here because the buttons depend on it.
|
||
;; Moreover, its value is overwritten after enabling the major
|
||
;; mode, so it needs to be set after.
|
||
(setq-local denote-directory dirs)
|
||
(setq overlay-arrow-position nil)
|
||
(goto-char (point-min))
|
||
(xref--insert-xrefs xref-alist)
|
||
(goto-char (point-min))
|
||
(setq-local revert-buffer-function
|
||
(lambda (_ignore-auto _noconfirm)
|
||
(when-let* ((buffer-file-name file))
|
||
(denote--display-buffer-from-xref-alist xref-alist buffer-name display-buffer-action)))))
|
||
(display-buffer buffer-name display-buffer-action)))
|
||
|
||
(defun denote-make-backlinks-buffer (identifier buffer-name display-buffer-action)
|
||
"Create links' buffer called BUFFER-NAME for IDENTIFIER.
|
||
|
||
DISPLAY-BUFFER-ACTION is a `display-buffer' action and concomitant
|
||
alist, such as `denote-backlinks-display-buffer-action'."
|
||
(if-let* ((xref-alist (denote-retrieve-xref-alist-for-backlinks identifier)))
|
||
(denote--display-buffer-from-xref-alist xref-alist buffer-name display-buffer-action)
|
||
(error "No matches for identifier `%s'" identifier)))
|
||
|
||
;; NOTE 2025-03-24: The `&rest' is there because we used to have an
|
||
;; extra SHOW-CONTEXT parameter. This way we do not break anybody's
|
||
;; code, even if we slightly modify the behaviour.
|
||
(defun denote-make-links-buffer (query &optional files buffer-name display-buffer-action &rest _)
|
||
"Create links' buffer called BUFFER-NAME for QUERY.
|
||
|
||
Optional FILES can be a list of files to search for. It can also be a
|
||
regexp, which limits the files accordingly per `denote-directory-files'.
|
||
|
||
Optional DISPLAY-BUFFER-ACTION is a `display-buffer' action and
|
||
concomitant alist, such as `denote-backlinks-display-buffer-action'."
|
||
(if-let* ((inhibit-read-only t)
|
||
(buffer (or buffer-name
|
||
(denote-format-buffer-name (format-message "query for `%s'" query) :special-buffer)))
|
||
(xref-alist (denote-retrieve-xref-alist query files))
|
||
(files (delete-dups (mapcar #'car xref-alist))))
|
||
(progn
|
||
(setq denote-query--last-query query)
|
||
(setq denote-query--last-files files)
|
||
(denote--display-buffer-from-xref-alist xref-alist buffer display-buffer-action))
|
||
(error "No matches for query `%s'" query)))
|
||
|
||
(defvar denote-query-links-buffer-function #'denote-make-links-buffer
|
||
"Function to make an Xref buffer showing query link results.
|
||
It accepts the same arguments as `denote-make-links-buffer'.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-grep-query-prompt
|
||
'denote-query-prompt
|
||
"4.2.0")
|
||
|
||
(defvar denote-query-prompt-history nil
|
||
"Minibuffer history for `denote-query-prompt'.")
|
||
|
||
(defun denote-query-prompt (&optional prompt-text)
|
||
"Prompt for a grep query in the minibuffer.
|
||
With optional PROMPT-TEXT use it for the minibuffer prompt.
|
||
|
||
For backward-compatibility, PROMPT-TEXT can also be a keyword among
|
||
`:focused', `:dired', and `:region', to format the prompt accordingly
|
||
for the given type of search. Developers should not rely on this, as we
|
||
will remove it in future versions of Denote---just use PROMPT-TEXT as a
|
||
string."
|
||
(let ((default (car denote-query-prompt-history)))
|
||
(read-string
|
||
(format-prompt
|
||
(pcase prompt-text
|
||
((pred stringp) prompt-text)
|
||
(:focus
|
||
"Search for REGEXP in all files")
|
||
(:dired
|
||
"Search for REGEXP in marked Dired files")
|
||
(:region
|
||
"Search for REGEXP in the active region files")
|
||
(:include
|
||
"Only show files whose name matches REGEXP")
|
||
(:exclude
|
||
"Exclude files whose name matches REGEXP")
|
||
(_ "Search (all Denote files)"))
|
||
default)
|
||
nil 'denote-query-prompt-history default)))
|
||
|
||
(defun denote-query-focus-last-search (query)
|
||
"Search QUERY in the content of files in the current Denote query buffer.
|
||
A query buffer is one that contains the results of commands such as
|
||
`denote-grep', `denote-backlinks', `denote-query-contents-link', or,
|
||
generally, any command that relies on the `denote-make-links-buffer'."
|
||
(interactive
|
||
(or (denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(list (denote-query-prompt :focus)))
|
||
denote-query-mode)
|
||
(denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(denote-make-links-buffer query denote-query--last-files nil '(display-buffer-same-window))
|
||
(message "Searching `%s' in files: `%S'" query denote-query--last-files))
|
||
|
||
(defun denote-query--filter-files (regexp include-p)
|
||
"Filter `denote-query--last-files' with REGEXP.
|
||
If INCLUDE-P is non-nil, only include matches. Otherwise exclude them."
|
||
(if-let* ((filter-fn (if include-p #'seq-filter #'seq-remove))
|
||
(final-files (funcall filter-fn (lambda (file) (string-match-p regexp file)) denote-query--last-files)))
|
||
(denote-make-links-buffer denote-query--last-query final-files (buffer-name) '(display-buffer-same-window))
|
||
(user-error "No remaining files when applying that filter")))
|
||
|
||
(defun denote-query-exclude-files (regexp)
|
||
"Exclude files matching REGEXP from the current Denote query buffer.
|
||
REGEXP is matched against the file name."
|
||
(interactive
|
||
(or (denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(list (denote-query-prompt :exclude)))
|
||
denote-query-mode)
|
||
(denote-query--filter-files regexp nil))
|
||
|
||
(defun denote-query-only-include-files (regexp)
|
||
"Only show files matching REGEXP in the current Denote query buffer.
|
||
REGEXP is matched against the file name."
|
||
(interactive
|
||
(or (denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(list (denote-query-prompt :include)))
|
||
denote-query-mode)
|
||
(denote-query--filter-files regexp :include))
|
||
|
||
(defun denote-query--keywords-as-regexp (keywords)
|
||
"Return KEYWORDS as a single regular expression.
|
||
KEYWORDS is a list of strings."
|
||
(if (seq-every-p #'stringp keywords)
|
||
(format "_%s" (regexp-opt keywords))
|
||
(error "KEYWORDS must be a list of strings; got `%S'" keywords)))
|
||
|
||
(defun denote-query--filter-keywords (keywords include-p)
|
||
"Filter `denote-query--last-files' with KEYWORDS.
|
||
If INCLUDE-P is non-nil, only include matches. Otherwise exclude them."
|
||
(denote-query--filter-files (denote-query--keywords-as-regexp keywords) include-p))
|
||
|
||
(defun denote-query-exclude-files-with-keywords (keywords)
|
||
"Exclude files with KEYWORDS from current query buffer.
|
||
|
||
KEYWORDS should be a list of keywords (without underscore).
|
||
|
||
Interactively, KEYWORDS are read from the minibuffer using
|
||
`completing-read-multiple', which see."
|
||
(interactive
|
||
(or (denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(list (denote-keywords-prompt "Exclude files with keywords")))
|
||
denote-query-mode)
|
||
(denote-query--filter-keywords keywords nil))
|
||
|
||
(defun denote-query-only-include-files-with-keywords (keywords)
|
||
"Only show files with KEYWORDS in the current Denote query buffer.
|
||
|
||
See `denote-query-exclude-files-with-keywords' for details."
|
||
(interactive
|
||
(or (denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(list (denote-keywords-prompt "Only include files with keywords")))
|
||
denote-query-mode)
|
||
(denote-query--filter-keywords keywords :include))
|
||
|
||
(defun denote-query-clear-all-filters ()
|
||
"Run last search with the full set of files in the variable `denote-directory'.
|
||
|
||
This effectively gets ride of any interactive filter applied (by the
|
||
means of e.g. `denote-query-exclude-files')."
|
||
(interactive nil denote-query-mode)
|
||
(denote--user-error-if-not-major-mode 'denote-query-mode)
|
||
(denote-make-links-buffer denote-query--last-query nil (buffer-name) '(display-buffer-same-window))
|
||
(message "Cleared all filters"))
|
||
|
||
(defun denote-query-sort-last-search (component)
|
||
"Sort files matched by the last search according to COMPONENT.
|
||
|
||
When called interactively, prompt for COMPONENT among `denote-sort-components'.
|
||
|
||
When called from Lisp, COMPONENT has the same meaning as in the function
|
||
`denote-sort-files'."
|
||
(interactive (list (denote-sort-component-prompt)))
|
||
(let ((denote-query-sorting component))
|
||
(denote-make-links-buffer denote-query--last-query denote-query--last-files (buffer-name) '(display-buffer-same-window))))
|
||
|
||
;;;;;; Additional features for searching file contents
|
||
|
||
;; TODO 2026-04-06: I need to review `denote-grep' and all its
|
||
;; ancillary functions.
|
||
|
||
;; NOTE 2025-12-12: Unlike `denote-query-links-display-buffer-action'
|
||
;; we want `denote-grep' to behave like `denote-dired', whereby
|
||
;; `dired' normally works in the current window.
|
||
(defcustom denote-grep-display-buffer-action
|
||
'((display-buffer-same-window)
|
||
(mode . denote-query-mode))
|
||
"`display-buffer' action for the results of `denote-grep'.
|
||
This is the same as `denote-backlinks-display-buffer-action'. Refer to
|
||
its documentation for the technicalities."
|
||
:risky t
|
||
:type `(choice
|
||
(alist :key-type
|
||
(choice :tag "Condition"
|
||
regexp
|
||
(function :tag "Matcher function"))
|
||
:value-type ,display-buffer--action-custom-type)
|
||
(function :tag "Custom function to return an action alist"))
|
||
:package-version '(denote . "4.0.0")
|
||
:group 'denote-query)
|
||
|
||
(make-obsolete-variable 'denote-grep-history 'denote-query-prompt-history "4.2.0")
|
||
(make-obsolete-variable 'denote-grep-file-regexp-history 'denote-query-prompt-history "4.2.0")
|
||
(make-obsolete 'denote-grep-file-regexp-prompt 'denote-query-prompt "4.2.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-grep (query)
|
||
"Search QUERY in the content of Denote files.
|
||
QUERY should be a regular expression accepted by `xref-search-program'.
|
||
|
||
The files to search for are those returned by `denote-directory-files'
|
||
with a non-nil TEXT-ONLY argument.
|
||
|
||
Results are put in a buffer which allows folding and further
|
||
filtering (see the manual for details).
|
||
|
||
You can insert a link to a grep search in any note by using the command
|
||
`denote-query-contents-link'."
|
||
(interactive (list (denote-query-prompt)))
|
||
(let ((denote-query--omit-current nil))
|
||
(denote-make-links-buffer query nil nil denote-grep-display-buffer-action)))
|
||
|
||
;;;###autoload
|
||
(defun denote-grep-marked-dired-files (query)
|
||
"Do the equivalent of `denote-grep' for QUERY in marked Dired files."
|
||
(interactive (list (denote-query-prompt :dired)))
|
||
(if-let* ((files (dired-get-marked-files)))
|
||
(denote-make-links-buffer query files nil denote-grep-display-buffer-action)
|
||
(user-error "No marked files")))
|
||
|
||
(defun denote-grep--get-files-referenced-in-region (start end)
|
||
"Return a list with all Denote files referenced between START and END.
|
||
START and END are buffer positions, as integers. A reference to a file
|
||
is the mere presence of its identifier.
|
||
|
||
Return a list with the absoulte path of referenced files."
|
||
(let ((id-list nil))
|
||
(save-excursion
|
||
(save-restriction
|
||
(narrow-to-region start end)
|
||
(goto-char (point-min))
|
||
(while (re-search-forward denote-date-identifier-regexp nil t)
|
||
(push (denote-get-path-by-id (match-string 0)) id-list))))
|
||
id-list))
|
||
|
||
;;;###autoload
|
||
(defun denote-grep-files-referenced-in-region (query start end)
|
||
"Perform `denote-grep' QUERY in files referenced between START and END.
|
||
When called interactively, prompt for QUERY. Also get START and END as
|
||
the buffer positions that delimit the marked region. When called from
|
||
Lisp, QUERY is a string, while START and END are buffer positions, as
|
||
integers.
|
||
|
||
Find references to files by their identifier. This includes links with
|
||
just the identifier (as described in `denote-link' and related), links
|
||
written by an Org dynamic block (see the `denote-org' package), or even
|
||
file listings such as those of `dired' and the command-line `ls' program."
|
||
(interactive
|
||
(if (region-active-p)
|
||
(list
|
||
(denote-query-prompt :region)
|
||
(region-beginning)
|
||
(region-end))
|
||
(user-error "No region is active; aborting")))
|
||
(if-let* ((files (denote-grep--get-files-referenced-in-region start end)))
|
||
(denote-make-links-buffer query files nil denote-grep-display-buffer-action)
|
||
(user-error "No files referenced in region")))
|
||
|
||
;;;;;; Backlinks
|
||
|
||
(defun denote--backlinks-get-buffer-name (file id)
|
||
"Format a buffer name for `denote-backlinks'.
|
||
Use FILE to detect a suitable title with which to name the buffer. Else
|
||
use the ID."
|
||
(denote-format-buffer-name
|
||
(if-let* ((type (denote-filetype-heuristics file))
|
||
(title (denote-retrieve-front-matter-title-value file type)))
|
||
(format "FILE backlinks for %S" title)
|
||
(format "FILE backlinks for %s" id))
|
||
:special-buffer))
|
||
|
||
;;;###autoload
|
||
(defun denote-backlinks ()
|
||
"Produce a buffer with backlinks to the current note.
|
||
|
||
Show the names of files linking to the current file.
|
||
|
||
Place the buffer below the current window or wherever the user option
|
||
`denote-backlinks-display-buffer-action' specifies."
|
||
(interactive)
|
||
(if-let* ((file buffer-file-name))
|
||
(if-let* ((identifier (denote-retrieve-filename-identifier file)))
|
||
(denote-make-backlinks-buffer
|
||
identifier
|
||
(denote--backlinks-get-buffer-name file identifier)
|
||
denote-backlinks-display-buffer-action)
|
||
(user-error "The current file does not have a Denote identifier"))
|
||
(user-error "Buffer `%s' is not associated with a file" (current-buffer))))
|
||
|
||
(defalias 'denote-show-backlinks-buffer 'denote-backlinks
|
||
"Alias for `denote-backlinks' command.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-return-backlinks
|
||
'denote-get-backlinks
|
||
"4.1.0")
|
||
|
||
(defun denote-get-backlinks (&optional file)
|
||
"Return list of backlinks in current or optional FILE.
|
||
Also see `denote-get-links'."
|
||
(when-let* ((current-file (or file (buffer-file-name)))
|
||
(id (or (denote-retrieve-filename-identifier current-file)
|
||
(user-error "The file does not have a Denote identifier")))
|
||
(_ (denote-file-is-in-denote-directory-p current-file))
|
||
(xrefs (denote-retrieve-xref-alist-for-backlinks id)))
|
||
(mapcar #'car xrefs)))
|
||
|
||
(defun denote--file-has-backlinks-p (file)
|
||
"Return non-nil if FILE has backlinks."
|
||
(when-let* ((id (denote-retrieve-filename-identifier file))
|
||
(files (denote-directory-files nil :omit-current :text-only)))
|
||
(catch 'has-backlinks
|
||
(dolist (file files)
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(goto-char (point-min))
|
||
(when (search-forward (format "[denote:%s]" id) nil t)
|
||
(throw 'has-backlinks t)))))))
|
||
|
||
;;;###autoload
|
||
(defun denote-find-backlink ()
|
||
"Use minibuffer completion to visit backlink to current file.
|
||
Visit the file itself, not the location where the link is. For a
|
||
context-sensitive operation, use `denote-find-backlink-with-location'.
|
||
|
||
Alo see `denote-find-link'."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(when-let* ((current-file buffer-file-name)
|
||
(_ (or (denote-retrieve-filename-identifier current-file)
|
||
(user-error "The current file does not have a Denote identifier")))
|
||
(links (or (denote-get-backlinks current-file)
|
||
(user-error "No backlinks found")))
|
||
(selected (denote-select-from-files-prompt links "Select among BACKLINKS")))
|
||
(find-file selected)))
|
||
|
||
;;;###autoload
|
||
(defun denote-find-backlink-with-location ()
|
||
"Like `denote-find-backlink' but jump to the exact location of the link."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(when-let* ((current-file buffer-file-name)
|
||
(id (or (denote-retrieve-filename-identifier current-file)
|
||
(user-error "The current file does not have a Denote identifier")))
|
||
(files (denote-directory-files nil :omit-current :text-only))
|
||
(fetcher (lambda () (xref-matches-in-files id files))))
|
||
(xref-show-definitions-completing-read fetcher nil)))
|
||
|
||
;;;;;; Query links
|
||
|
||
(defvar denote-query-link-history nil
|
||
"Minibuffer history of `denote-query-link-prompt'.")
|
||
|
||
(defun denote-query-link-prompt (&optional initial-query prompt-text)
|
||
"Prompt for query string.
|
||
With optional INITIAL-QUERY 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."
|
||
(when (and initial-query (string-empty-p initial-query))
|
||
(setq initial-query nil))
|
||
(denote--with-conditional-completion
|
||
'denote-query-link-prompt
|
||
(format-prompt (or prompt-text "Query for") nil)
|
||
denote-query-link-history
|
||
initial-query))
|
||
|
||
(defconst denote-query-link-types '(query-contents query-filenames)
|
||
"Types of query links.")
|
||
|
||
;; NOTE 2025-03-27: Should we expose a user option for this? And/or
|
||
;; should we add a DESCRIPTION parameter to `denote--format-query-link'?
|
||
;;
|
||
;; What would make for a good default description in that scenario?
|
||
;; Maybe "QC:query text here" and "QF:query text here" for
|
||
;; `query-contents' and `query-filenames' respectively.
|
||
(defvar denote-query-description-prefix ""
|
||
"Prefix string for query links to format their description text.
|
||
The description text constists of the value of this variable followed by
|
||
the query")
|
||
|
||
(defun denote--format-query-link (type query file-type)
|
||
"Format QUERY link of TYPE for the given FILE-TYPE.
|
||
Return an error if TYPE is not one among the symbols specified in
|
||
`denote-query-link-types'.
|
||
|
||
If FILE-TYPE is nil, use that of Org."
|
||
(unless (memq type denote-query-link-types)
|
||
(error "Type `%s' is not one among `denote-query-link-types'" type))
|
||
(format (or (denote--link-format file-type) (denote--link-format 'org))
|
||
(format "%s:%s" type query)
|
||
(format "%s%s" denote-query-description-prefix query)))
|
||
|
||
;;;###autoload
|
||
(defun denote-query-contents-link (query)
|
||
"Insert query link for file contents.
|
||
Prompt for QUERY or use the text of the active region. When the user
|
||
follows this link, place any matches in a separate buffer (using the
|
||
built-in Xref mechanism). This is the equivalent of a Unix grep command
|
||
across the variable `denote-directory'."
|
||
(interactive
|
||
(list
|
||
(or (denote--get-active-region-content)
|
||
(denote-query-link-prompt nil "Query in file CONTENTS"))))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(denote--delete-active-region-content)
|
||
(insert (denote--format-query-link 'query-contents query (denote-filetype-heuristics buffer-file-name))))
|
||
|
||
;;;###autoload
|
||
(defun denote-query-filenames-link (query)
|
||
"Insert query link for file names.
|
||
Prompt for QUERY or use the text of the active region. When the user
|
||
follows this link, place any matches in a separate buffer (using the
|
||
built-in Dired mechanism). This is the equivalent of a Unix find
|
||
command across the variable `denote-directory'."
|
||
(interactive
|
||
(list
|
||
(or (denote--get-active-region-content)
|
||
(denote-query-link-prompt nil "Query in file NAMES"))))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(denote--delete-active-region-content)
|
||
(insert (denote--format-query-link 'query-filenames query (denote-filetype-heuristics buffer-file-name))))
|
||
|
||
(defvar denote--query-last-dired-buffer nil
|
||
"Buffer object produced by the last query for file names.")
|
||
|
||
(defun denote--act-on-query-link (query)
|
||
"Act on QUERY link.
|
||
QUERY is a string of the form TYPE:SEARCH, where TYPE is one among
|
||
`denote-query-link-types' while SEARCH is the regular expression to
|
||
search for."
|
||
(cond
|
||
((string-prefix-p "query-contents:" query)
|
||
(setq query (replace-regexp-in-string "query-contents:" "" query))
|
||
(funcall denote-query-links-buffer-function query nil nil denote-query-links-display-buffer-action))
|
||
((string-prefix-p "query-filenames:" query)
|
||
(setq query (replace-regexp-in-string "query-filenames:" "" query))
|
||
;; NOTE 2025-03-27: I do not think we need to add another
|
||
;; parameter to `denote-sort-dired' for handling the
|
||
;; `display-buffer'. This is a special case, but we can always
|
||
;; change it later if the need arises.
|
||
;;
|
||
;; Here we handle the buffer and window state to make it behave
|
||
;; like the Xref buffer. Otherwise, Dired does not reuse its
|
||
;; buffer (which is generally okay).
|
||
(let ((buffer (save-window-excursion
|
||
(denote-sort-dired query nil nil nil)
|
||
(current-buffer))))
|
||
(when (bufferp denote--query-last-dired-buffer)
|
||
(when-let* ((window (get-buffer-window denote--query-last-dired-buffer))
|
||
(_ (window-live-p window)))
|
||
(delete-window window))
|
||
(kill-buffer denote--query-last-dired-buffer))
|
||
(display-buffer buffer denote-query-links-display-buffer-action)
|
||
(setq denote--query-last-dired-buffer buffer)))
|
||
(t
|
||
(error "Cannot open `%s' of unknown link type" query))))
|
||
|
||
;;;;; Link buttons
|
||
|
||
(make-obsolete 'denote-link--find-file-at-button nil "4.0.0")
|
||
|
||
;; NOTE 2025-12-12: The `markdown-follow-link-functions' assumes that
|
||
;; the link of a specific format, but this is not good enough for us
|
||
;; because of the query links we support. I think it is okay to
|
||
;; ignore LINK and just act on the link at point.
|
||
(defun denote-link-markdown-follow (_link)
|
||
"Function for to act on Markdown link at point.
|
||
To be assigned to `markdown-follow-link-functions'."
|
||
(denote-link-open-at-point))
|
||
|
||
(eval-after-load 'markdown-mode
|
||
'(add-hook 'markdown-follow-link-functions #'denote-link-markdown-follow))
|
||
|
||
;;;;; Link fontification
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote-link-mouse-map
|
||
'denote-fontify-links-map
|
||
"4.2.0")
|
||
|
||
(defvar denote-fontify-links-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "RET") #'denote-link-open-at-point)
|
||
(define-key map (kbd "C-c C-o") #'denote-link-open-at-point)
|
||
(define-key map [mouse-1] #'denote-link-open-at-mouse)
|
||
(define-key map [mouse-2] #'denote-link-open-at-mouse)
|
||
(define-key map [mouse-3] #'denote-link-open-at-mouse)
|
||
map)
|
||
"Keymap for mouse actions over fontified Denote links.")
|
||
|
||
;; Adapted from `org-in-regexp'.
|
||
(defun denote--inside-link-regexp-p (regexp position)
|
||
"Check if POSITION is inside a Denote link REGEXP.
|
||
Return either nil or a list whose elements are two cons cells:
|
||
|
||
- The first cons cell has the link target and the link description,
|
||
like (\"denote:20250816T080008\" . \"This is a test\").
|
||
|
||
- The second cons cell consists of two buffer positions, pointing to the
|
||
beginning and end of REGEXP."
|
||
(catch 'exit
|
||
(let ((line-end (line-end-position)))
|
||
(save-excursion
|
||
(forward-line 0)
|
||
(while (and (re-search-forward regexp line-end t)
|
||
(<= (match-beginning 0) position))
|
||
(when (>= (match-end 0) position)
|
||
(throw 'exit (list (cons
|
||
(match-string-no-properties 1)
|
||
(match-string-no-properties 2))
|
||
(cons
|
||
(match-beginning 0)
|
||
(match-end 0))))))))))
|
||
|
||
(defun denote--link-at-point-get-data (position)
|
||
"Return matching data for the link at POSITION."
|
||
(when-let* ((file buffer-file-name)
|
||
(file-type (denote-filetype-heuristics file))
|
||
(regexp (denote--link-in-context-regexp file-type)))
|
||
(denote--inside-link-regexp-p regexp position)))
|
||
|
||
(defun denote--link-open-at-point-subr (position)
|
||
"Open link at POSITION.
|
||
This is the subroutine of `denote-link-open-at-point' and
|
||
`denote-link-open-at-mouse'."
|
||
(pcase-let* ((data (denote--link-at-point-get-data position))
|
||
(`(,target . ,_) (car data))
|
||
(path (denote-get-path-by-id target)))
|
||
(cond
|
||
(path (funcall denote-open-link-function path))
|
||
(target (denote--act-on-query-link target)))))
|
||
|
||
(defun denote-link-open-at-point ()
|
||
"Open Denote link at point."
|
||
(interactive)
|
||
(denote--link-open-at-point-subr (point)))
|
||
|
||
(defun denote-link-open-at-mouse (ev)
|
||
"Open Denote link for mouse EV click."
|
||
(interactive "e")
|
||
(mouse-set-point ev)
|
||
(denote--link-open-at-point-subr (point)))
|
||
|
||
(defun denote-get-link-face (query)
|
||
"Return appropriate face for QUERY."
|
||
(if (or (string-prefix-p "query-contents:" query)
|
||
(string-prefix-p "query-filenames:" query))
|
||
'denote-faces-query-link
|
||
'denote-faces-link))
|
||
|
||
(defvar-local denote-fontify-links--data nil
|
||
"Cons cell of (FILE-TYPE . LINK-QUERY) for `denote-fontify-links'.")
|
||
|
||
(defun denote-fontify-links--get-data (force)
|
||
"Return `denote-fontify-links--datadenote-fontify-links--get-data'.
|
||
With non-nil FORCE compute the data outright, else first try to use what
|
||
is stored in `denote-fontify-links--data' and only compute the data anew
|
||
if needed."
|
||
(let ((file-type (denote-filetype-heuristics buffer-file-name)))
|
||
(if force
|
||
(cons file-type (denote--link-in-context-regexp file-type))
|
||
(or denote-fontify-links--data (denote-fontify-links--get-data :force)))))
|
||
|
||
(defun denote-fontify-links--set-data ()
|
||
"Set `denote-fontify-links--data'.
|
||
Use optional DATA, else get the data with `denote-fontify-links--get-data'."
|
||
(setq-local denote-fontify-links--data (denote-fontify-links--get-data :force)))
|
||
|
||
;; Implementation based on the function `org-activate-links'.
|
||
(defun denote-fontify-links (limit)
|
||
"Provide font-lock matcher to fontify links up to LIMIT."
|
||
(pcase-let ((`(,type . ,query) (denote-fontify-links--get-data nil)))
|
||
(when (and type query)
|
||
(catch 'exit
|
||
(while (re-search-forward query limit t)
|
||
(let* ((start (match-beginning 0))
|
||
(end (match-end 0))
|
||
(visible-start (or (match-beginning 2) start))
|
||
(visible-end (or (match-end 2) end))
|
||
(query (match-string-no-properties 1)))
|
||
(let* ((properties `( mouse-face highlight
|
||
keymap ,denote-fontify-links-map
|
||
denote-link-query-part ,query
|
||
help-echo query
|
||
htmlize-link (:uri ,query)
|
||
font-lock-multiline t))
|
||
(non-sticky-props
|
||
'(rear-nonsticky (mouse-face highlight keymap invisible help-echo htmlize-link)))
|
||
(face-property (denote-get-link-face query))
|
||
(hidden (append '(invisible 'denote-fontified-link) properties)))
|
||
(remove-text-properties start end '(invisible nil))
|
||
(add-text-properties start visible-start hidden)
|
||
(add-face-text-property start end face-property)
|
||
(add-text-properties visible-start visible-end properties)
|
||
(add-text-properties visible-end end hidden)
|
||
(dolist (pos (list end visible-start visible-end))
|
||
(add-text-properties (1- pos) pos non-sticky-props)))
|
||
(throw 'exit t)))
|
||
nil))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-get-identifier-at-point
|
||
'denote-get-link-identifier-or-query-term-at-point
|
||
"4.0.0")
|
||
|
||
(defun denote-get-link-identifier-or-query-term-at-point (&optional position)
|
||
"Return the Denote identifier or query term at point or optional POSITION."
|
||
(let* ((data (denote--link-at-point-get-data (or position (point))))
|
||
(target (caar data)))
|
||
target))
|
||
|
||
(defun denote--get-link-file-path-at-point ()
|
||
"Return target file path of the Denote link at point.
|
||
To be used as a `thing-at' provider."
|
||
(let* ((data (denote--link-at-point-get-data (point)))
|
||
(target (caar data)))
|
||
(when-let* ((path (denote-get-path-by-id target)))
|
||
(concat "file:" path))))
|
||
|
||
(defvar thing-at-point-provider-alist)
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-fontify-links-mode-maybe
|
||
'denote-fontify-links-mode
|
||
"4.2.0")
|
||
|
||
;;;###autoload
|
||
(define-minor-mode denote-fontify-links-mode
|
||
"Fontify Denote links in plain text buffers.
|
||
Do so only when the current buffer is a Denote note and the major mode
|
||
is not `org-mode' or `markdown-mode' (or any major mode derived
|
||
therefrom)."
|
||
:init-value nil
|
||
:global nil
|
||
:group 'denote
|
||
(require 'thingatpt)
|
||
(if (and buffer-file-name
|
||
(not (derived-mode-p 'org-mode 'markdown-mode))
|
||
(denote-file-is-in-denote-directory-p buffer-file-name)
|
||
(denote-file-has-supported-extension-p buffer-file-name)
|
||
(denote-file-has-denoted-filename-p buffer-file-name))
|
||
(progn
|
||
(if denote-fontify-links-mode
|
||
(progn
|
||
(add-to-invisibility-spec 'denote-fontified-link)
|
||
(denote-fontify-links--set-data)
|
||
(font-lock-add-keywords nil '((denote-fontify-links)))
|
||
(setq-local thing-at-point-provider-alist
|
||
(append thing-at-point-provider-alist
|
||
'((url . denote--get-link-file-path-at-point)))))
|
||
(remove-from-invisibility-spec 'denote-fontified-link)
|
||
(kill-local-variable 'denote-fontify-links--data)
|
||
(font-lock-remove-keywords nil '((denote-fontify-links)))
|
||
(setq-local thing-at-point-provider-alist
|
||
(delete
|
||
'(url . denote--get-link-file-path-at-point)
|
||
thing-at-point-provider-alist)))
|
||
(font-lock-update))
|
||
;; NOTE 2026-01-02: If we do not set the value here, then it is
|
||
;; toggled on/off even though the above `if' never reaches its
|
||
;; THEN branch.
|
||
(setq denote-fontify-links-mode nil)
|
||
;; NOTE 2026-01-02: In interactive use, we get a message that the
|
||
;; mode is disabled if we call it in non-supported buffers. I
|
||
;; tried to `let' bind the `inhibit-message' but that did not
|
||
;; work. So I am doing this instead...
|
||
(when (called-interactively-p 'interactive)
|
||
(message "`denote-fontify-links-mode' works only in plain text buffers inside the `denote-directory'"))))
|
||
|
||
;;;;; Add links matching regexp
|
||
|
||
(defvar denote-link--prepare-links-format "- %s\n"
|
||
"Format specifiers for `denote-add-links'.")
|
||
|
||
(defun denote-link--prepare-links (files current-file-type id-only &optional no-sort)
|
||
"Prepare links to FILES from CURRENT-FILE-TYPE.
|
||
When ID-ONLY is non-nil, use a generic link format.
|
||
|
||
With optional NO-SORT do not try to sort the inserted lines.
|
||
Otherwise sort lines while accounting for `denote-link-add-links-sort'."
|
||
(let ((links))
|
||
(dolist (file files)
|
||
(let* ((description (denote-get-link-description file current-file-type))
|
||
(link (denote-format-link file description current-file-type id-only))
|
||
(link-as-list-item (format denote-link--prepare-links-format link)))
|
||
(push link-as-list-item links)))
|
||
(if no-sort
|
||
(nreverse links)
|
||
(sort links #'string-collate-lessp))))
|
||
|
||
(defun denote-link--insert-links (files current-file-type &optional id-only no-sort)
|
||
"Insert at point a typographic list of links matching FILES.
|
||
|
||
With CURRENT-FILE-TYPE as a symbol among those specified in 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.
|
||
|
||
Optional NO-SORT is passed to `denote-link--prepare-links'."
|
||
(when-let* ((links (denote-link--prepare-links files current-file-type id-only no-sort)))
|
||
(dolist (link links)
|
||
(insert link))))
|
||
|
||
;;;###autoload
|
||
(defun denote-add-links (regexp &optional id-only)
|
||
"Insert links to all files whose file name matches REGEXP.
|
||
Use this command to reference multiple files at once. Particularly
|
||
useful for the creation of metanotes (read the manual for more on the
|
||
matter).
|
||
|
||
Optional ID-ONLY has the same meaning as in `denote-link': it
|
||
inserts links with just the identifier.
|
||
|
||
Also see `denote-link-to-all-files-with-contents'."
|
||
(interactive
|
||
(list
|
||
(denote-files-matching-regexp-prompt "Insert links to files matching REGEXP")
|
||
current-prefix-arg))
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The current file type is not recognized by Denote"))
|
||
(let ((file-type (denote-filetype-heuristics (buffer-file-name))))
|
||
(if-let* ((files (denote-directory-files regexp :omit-current nil nil :has-identifier)))
|
||
(denote-link--insert-links files file-type id-only)
|
||
(message "No links matching `%s'" regexp))))
|
||
|
||
(defalias 'denote-link-to-all-files-with-regexp 'denote-add-links
|
||
"Alias for `denote-add-links'.")
|
||
|
||
;;;;; Link to file with matching contents
|
||
|
||
;;;###autoload
|
||
(defun denote-link-to-file-with-contents (query &optional id-only)
|
||
"Link to a file whose contents match QUERY.
|
||
This is similar to `denote-link', except that the file prompt is limited
|
||
to files matching QUERY. Optional ID-ONLY has the same meaning as in
|
||
`denote-link'."
|
||
(interactive
|
||
(list (denote-query-link-prompt nil "Files whose contents include QUERY")))
|
||
(if-let* ((files (denote-retrieve-files-xref-query query))
|
||
;; NOTE 2025-03-29: Maybe we should have a named prompt
|
||
;; for this case, but I think we do not need it right now.
|
||
(file (completing-read
|
||
(format "Select FILE with contents `%s': "
|
||
(propertize query 'face 'denote-faces-prompt-current-name))
|
||
(denote-get-completion-table files '(category . file))
|
||
nil t nil 'denote-file-history)))
|
||
(denote-link file
|
||
(denote-filetype-heuristics buffer-file-name)
|
||
(denote-get-link-description file)
|
||
id-only)
|
||
(user-error "No files include the query `%s' in their contents" query)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-to-all-files-with-contents (query &optional id-only)
|
||
"Link to all files whose contents match QUERY.
|
||
This is similar to `denote-add-links', except it searches inside file
|
||
contents, not file names. Optional ID-ONLY has the same meaning as in
|
||
`denote-link' and `denote-add-links'."
|
||
(interactive
|
||
(list (denote-query-link-prompt nil "Files whose contents include QUERY")))
|
||
(if-let* ((files (denote-retrieve-files-xref-query query)))
|
||
(denote-link--insert-links files (denote-filetype-heuristics buffer-file-name) id-only)
|
||
(user-error "No files include the query `%s' in their contents" query)))
|
||
|
||
;;;;; Links from Dired marks
|
||
|
||
;; TODO 2025-04-29: Rewrite the denote-link--buffer-file-prompt to be more general.
|
||
;; TODO 2025-04-29: Rewrite `denote-link-dired-marked-notes' to be more easy to reason about.
|
||
|
||
;; NOTE 2022-07-21: I don't think we need a history for this one.
|
||
(defun denote-link--buffer-file-prompt (buffer-file-names)
|
||
"Select file from BUFFER-FILE-NAMES of Denote notes."
|
||
(let* ((roots (denote-directories))
|
||
(single-dir-p (null (cdr roots)))
|
||
(file-names (if single-dir-p
|
||
(mapcar
|
||
(lambda (file)
|
||
(denote--get-file-name-relative-to-directories file roots))
|
||
buffer-file-names)
|
||
buffer-file-names))
|
||
(selected (completing-read
|
||
"Select open note to add links to: "
|
||
(denote-get-completion-table file-names '(category . file))
|
||
nil t)))
|
||
(if single-dir-p
|
||
(expand-file-name selected roots)
|
||
selected)))
|
||
|
||
(defun denote-link--map-over-notes ()
|
||
"Return list of `denote-file-has-denoted-filename-p' from Dired marked items."
|
||
(seq-filter
|
||
(lambda (file)
|
||
(and (denote-file-has-denoted-filename-p file)
|
||
(denote-file-has-identifier-p file)))
|
||
(dired-get-marked-files)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-dired-marked-notes (files buffer &optional id-only)
|
||
"Insert Dired marked FILES as links in BUFFER.
|
||
|
||
FILES conform with the Denote file-naming scheme, such that they can be
|
||
linked to using the `denote:' link type.
|
||
|
||
The BUFFER is one which visits a Denote note file. If there are
|
||
multiple BUFFER candidates in buffers, prompt with completion for
|
||
one among them. If there is none, throw an error.
|
||
|
||
With optional ID-ONLY as a prefix argument, insert links with
|
||
just the identifier (same principle as with `denote-link').
|
||
|
||
This command is meant to be used from a Dired buffer."
|
||
(interactive
|
||
(if (derived-mode-p 'dired-mode)
|
||
(list
|
||
(denote-link--map-over-notes)
|
||
(let ((file-names (denote--buffer-file-names)))
|
||
(find-buffer-visiting
|
||
(cond
|
||
((null file-names)
|
||
(user-error "No buffers visiting Denote notes"))
|
||
((eq (length file-names) 1)
|
||
(car file-names))
|
||
(t
|
||
(denote-link--buffer-file-prompt file-names)))))
|
||
current-prefix-arg)
|
||
(user-error "This command only works inside a Dired buffer"))
|
||
dired-mode)
|
||
(when (null files)
|
||
(user-error "No note files to link to"))
|
||
(unless (buffer-live-p buffer)
|
||
(error "The buffer `%s' is not live" buffer))
|
||
(let ((body (lambda ()
|
||
(unless (or (denote--file-type-org-extra-p)
|
||
(and buffer-file-name (denote-file-has-supported-extension-p buffer-file-name)))
|
||
(user-error "The target file's type is not recognized by Denote"))
|
||
(when (y-or-n-p (format "Create links at point in `%s'?" buffer))
|
||
(denote-link--insert-links files (denote-filetype-heuristics buffer-file-name) id-only)
|
||
(message "Added links to `%s'; displaying it now"
|
||
;; TODO 2024-12-26: Do we need our face here? I think
|
||
;; not, but let me keep a note of it.
|
||
(propertize (format "%s" buffer) 'face 'success))))))
|
||
(if-let* ((window (get-buffer-window buffer))
|
||
((window-live-p window)))
|
||
(with-selected-window window (funcall body))
|
||
(with-current-buffer buffer (funcall body))
|
||
(display-buffer-below-selected buffer nil))))
|
||
|
||
(defalias 'denote-dired-link-marked-notes 'denote-link-dired-marked-notes
|
||
"Alias for `denote-link-dired-marked-notes' command.")
|
||
|
||
;;;; Define menu
|
||
|
||
(defvar denote--menu-contents
|
||
'("Denote"
|
||
["Create a note" denote
|
||
:help "Create a new note in the `denote-directory'"]
|
||
["Create a note with given file type" denote-type
|
||
:help "Create a new note with a given file type in the `denote-directory'"]
|
||
["Create a note in subdirectory" denote-subdirectory
|
||
:help "Create a new note in a subdirectory of the `denote-directory'"]
|
||
["Create a note with date" denote-date
|
||
:help "Create a new note with a given date in the `denote-directory'"]
|
||
["Create a note with signature" denote-signature
|
||
:help "Create a new note with a given signature in the `denote-directory'"]
|
||
["Open a note or create it if missing" denote-open-or-create
|
||
:help "Open an existing note in the `denote-directory' or create it if missing"]
|
||
["Open a note or create it with the chosen command" denote-open-or-create-with-command
|
||
:help "Open an existing note or create it with the chosen command if missing"]
|
||
"---"
|
||
["Rename a file" denote-rename-file
|
||
:help "Rename file interactively"
|
||
:enable (derived-mode-p 'dired-mode 'text-mode)]
|
||
["Rename this file using its front matter" denote-rename-file-using-front-matter
|
||
:help "Rename the current file using its front matter as input"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Rename Dired marked files interactively" denote-dired-rename-files
|
||
:help "Rename marked files in Dired by prompting for all file name components"
|
||
:enable (derived-mode-p 'dired-mode)]
|
||
["Rename Dired marked files with keywords" denote-dired-rename-marked-files-with-keywords
|
||
:help "Rename marked files in Dired by prompting for keywords"
|
||
:enable (derived-mode-p 'dired-mode)]
|
||
["Rename Dired marked files using their front matter" denote-dired-rename-marked-files-using-front-matter
|
||
:help "Rename marked files in Dired using their front matter as input"
|
||
:enable (derived-mode-p 'dired-mode)]
|
||
"---"
|
||
["Insert a direct link" denote-link
|
||
:help "Insert link to a file in the `denote-directory'"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Insert a direct link to file with contents" denote-link-to-file-with-contents
|
||
:help "Insert link to a file in the `denote-directory' whose contents include a query"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Insert a query link for file contents" denote-query-contents-link
|
||
:help "Insert query link searching for file contents in the `denote-directory'"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Insert a query link for file names" denote-query-filenames-link
|
||
:help "Insert query link searching for file names in the `denote-directory'"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
"---"
|
||
["Insert links to file names matching regexp" denote-add-links
|
||
:help "Insert links to file names in the `denote-directory' matching regexp"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Insert links to files whose contents match regexp" denote-link-to-all-files-with-contents
|
||
:help "Insert links to file in the `denote-directory' whose contents match regexp"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Insert Dired marked files as links" denote-link-dired-marked-notes
|
||
:help "Rename marked files in Dired as links in a Denote buffer"
|
||
:enable (derived-mode-p 'dired-mode)]
|
||
["Show file backlinks" denote-backlinks
|
||
:help "Insert link to a file in the `denote-directory'"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Link to existing note or newly created one" denote-link-or-create
|
||
:help "Insert a link to an existing file, else create it and link to it"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Link to existing note or newly created one with the chosen command" denote-link-or-create-with-command
|
||
:help "Insert a link to an existing file, else create it with the given command and link to it"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Create note in the background and link to it directly" denote-link-after-creating
|
||
:help "Create new note and link to it from the current file"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Create note in the background with chosen command and link to it directly" denote-link-after-creating-with-command
|
||
:help "Create new note with the chosen command and link to it from the current file"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
"---"
|
||
["Generate sorted and filtered Dired listing" denote-sort-dired
|
||
:help "Generate a sorted and filtered Dired listing of files in the `denote-directory'"]
|
||
["Perform a query in the contents of files" denote-grep
|
||
:help "Search inside files in the `denote-directory'"]
|
||
"---"
|
||
["Search inside the files of the last search (focused search)" denote-query-exclude-files
|
||
:help "Perform a query inside only the files that matched the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
["Exclude files from last search" denote-query-exclude-files
|
||
:help "Exclude files matching a regular expression from the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
["Exclude files with keywords from last search" denote-query-exclude-files-with-keywords
|
||
:help "Exclude files matching the given keywords from the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
["Include only the given files from last search" denote-query-only-include-files
|
||
:help "Include only the files matching the given regular expression from the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
["Include only the given files with keywords from last search" denote-query-only-include-files-with-keywords
|
||
:help "Include only the files matching the given keywords from the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
["Sort files of last search" denote-query-sort-last-search
|
||
:help "Change the order of the files matched in the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
["Clear all filters from last search" denote-query-clear-all-filters
|
||
:help "Clear all the filters that have been applied to the last search"
|
||
:enable (derived-mode-p 'denote-query-mode)]
|
||
"---"
|
||
["Highlight Dired file names" denote-dired-mode
|
||
:help "Apply colors to Denote file name components in Dired"
|
||
:enable (derived-mode-p 'dired-mode)
|
||
:style toggle
|
||
:selected (bound-and-true-p denote-dired-mode)])
|
||
"Contents of the Denote menu.")
|
||
|
||
(defun denote--menu-bar-enable ()
|
||
"Enable Denote menu bar."
|
||
(define-key-after global-map [menu-bar denote]
|
||
(easy-menu-binding
|
||
(easy-menu-create-menu "Denote" denote--menu-contents) "Denote")
|
||
"Tools"))
|
||
|
||
;; Enable Denote menu bar by default
|
||
(denote--menu-bar-enable)
|
||
|
||
;;;###autoload
|
||
(define-minor-mode denote-menu-bar-mode "Show Denote menu bar."
|
||
:global t
|
||
:init-value t
|
||
(if denote-menu-bar-mode
|
||
(denote--menu-bar-enable)
|
||
(define-key global-map [menu-bar denote] nil)))
|
||
|
||
(defun denote-context-menu (menu _click)
|
||
"Populate MENU with Denote commands at CLICK."
|
||
(define-key menu [denote-separator] menu-bar-separator)
|
||
(let ((easy-menu (make-sparse-keymap "Denote")))
|
||
(easy-menu-define nil easy-menu nil
|
||
denote--menu-contents)
|
||
(dolist (item (reverse (lookup-key easy-menu [menu-bar])))
|
||
(when (consp item)
|
||
(define-key menu (vector (car item)) (cdr item)))))
|
||
menu)
|
||
|
||
;;;; Register `denote:' custom Org hyperlink
|
||
|
||
(declare-function org-link-open-as-file "ol" (path arg))
|
||
|
||
(defun denote-link--ol-resolve-link-to-target (link &optional full-data)
|
||
"Resolve LINK to target file, with or without additioanl file-search terms.
|
||
With optional FULL-DATA return a list in the form of (path query file-search)."
|
||
(let* ((file-search (and (string-match "::\\(.*\\)\\'" link)
|
||
(match-string 1 link)))
|
||
(query (if (and file-search (not (string-empty-p file-search)))
|
||
(substring link 0 (match-beginning 0))
|
||
link))
|
||
(path (denote-get-path-by-id query)))
|
||
(cond
|
||
(full-data
|
||
(list path query file-search))
|
||
((and file-search (not (string-empty-p file-search)))
|
||
(concat path "::" file-search))
|
||
(t (or path query)))))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-ol-follow (link)
|
||
"Find file of type `denote:' matching LINK.
|
||
LINK is the identifier of the note, optionally followed by a file search
|
||
option akin to that of standard Org `file:' link types. Read Info
|
||
node `(org) Query Options'.
|
||
|
||
If LINK is not an identifier, then it is not pointing to a file but to a
|
||
query of file contents or file names (see the commands
|
||
`denote-query-contents-link' and `denote-query-filenames-link').
|
||
|
||
Uses the function `denote-directory' to establish the path to the file."
|
||
(if-let* ((match (denote-link--ol-resolve-link-to-target link))
|
||
(_ (file-exists-p (string-trim-right match "::.*"))))
|
||
(org-link-open-as-file match nil)
|
||
(denote--act-on-query-link match)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-ol-complete ()
|
||
"Like `denote-link' but for Org integration.
|
||
This lets the user complete a link through the `org-insert-link'
|
||
interface by first selecting the `denote:' hyperlink type."
|
||
(if-let* ((file (denote-file-prompt nil nil nil :has-identifier)))
|
||
(concat "denote:" (denote-retrieve-filename-identifier file))
|
||
(user-error "No files in `denote-directory'")))
|
||
|
||
(declare-function org-link-store-props "ol.el" (&rest plist))
|
||
(defvar org-store-link-plist)
|
||
|
||
(declare-function org-entry-put "org" (pom property value))
|
||
(declare-function org-entry-get "org" (pom property &optional inherit literal-nil))
|
||
(declare-function org-id-new "org-id" (&optional prefix))
|
||
|
||
(defun denote-link-ol-get-id ()
|
||
"Get the CUSTOM_ID of the current entry.
|
||
If the entry already has a CUSTOM_ID, return it as-is, else
|
||
create a new one."
|
||
(let* ((pos (point))
|
||
(id (org-entry-get pos "CUSTOM_ID")))
|
||
(if (and (stringp id) (string-match-p "\\S-" id))
|
||
id
|
||
(setq id (org-id-new "h"))
|
||
(org-entry-put pos "CUSTOM_ID" id)
|
||
id)))
|
||
|
||
(declare-function org-get-heading "org" (no-tags no-todo no-priority no-comment))
|
||
|
||
(defun denote-link-ol-get-heading ()
|
||
"Get current Org heading text."
|
||
(org-get-heading :no-tags :no-todo :no-priority :no-comment))
|
||
|
||
(defun denote-link-format-heading-description (file-text heading-text)
|
||
"Return description for FILE-TEXT with HEADING-TEXT at the end."
|
||
(format "%s::%s" file-text heading-text))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-ol-store (&optional _interactive?)
|
||
"Handler for `org-store-link' adding support for denote: links.
|
||
Optional INTERACTIVE? is used internally by `org-store-link'.
|
||
|
||
Also see the user option `denote-org-store-link-to-heading'."
|
||
(when-let* ((file (buffer-file-name))
|
||
((file-regular-p file))
|
||
((denote-file-is-in-denote-directory-p file))
|
||
((denote-file-has-denoted-filename-p file))
|
||
(file-id (denote-retrieve-filename-identifier file))
|
||
(description (denote-get-link-description file)))
|
||
(let ((heading-links (and denote-org-store-link-to-heading
|
||
(derived-mode-p 'org-mode)
|
||
(denote--org-capture-link-specifiers-p)))
|
||
(heading (denote-link-ol-get-heading)))
|
||
(org-link-store-props
|
||
:type "denote"
|
||
:description (if (and heading-links heading)
|
||
(denote-link-format-heading-description
|
||
description
|
||
heading)
|
||
description)
|
||
:link (cond
|
||
((when-let* ((id (org-entry-get (point) "CUSTOM_ID")))
|
||
(format "denote:%s::#%s" file-id id)))
|
||
((and heading-links (eq denote-org-store-link-to-heading 'context) heading)
|
||
(format "denote:%s::*%s" file-id heading))
|
||
((and heading-links heading)
|
||
(format "denote:%s::#%s" file-id (denote-link-ol-get-id)))
|
||
(t
|
||
(concat "denote:" file-id))))
|
||
org-store-link-plist)))
|
||
|
||
(defun denote-link--ol-export-get-relative-html (path)
|
||
"Return relative PATH for Org export purposes.
|
||
Add an .html extension if PATH is an Org file."
|
||
(file-relative-name
|
||
(if (string= (file-name-extension path) "org")
|
||
(concat (file-name-sans-extension path) ".html")
|
||
path)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-ol-export (link description format)
|
||
"Export a `denote:' link from Org files.
|
||
The LINK, DESCRIPTION, and FORMAT are handled by the export
|
||
backend."
|
||
(pcase-let* ((`(,path ,query ,file-search) (denote-link--ol-resolve-link-to-target link :full-data))
|
||
(desc (cond
|
||
(description)
|
||
(file-search (format "denote:%s::%s" query file-search))
|
||
(t (concat "denote:" query)))))
|
||
(if path
|
||
(pcase format
|
||
('html (format "<a href=\"%1$s%3$s\">%2$s</a>" (denote-link--ol-export-get-relative-html path) desc (or file-search "")))
|
||
('latex (format "\\href{%s}{%s}" (replace-regexp-in-string "[\\{}$%&_#~^]" "\\\\\\&" path) desc))
|
||
('texinfo (format "@uref{%s,%s}" path desc))
|
||
('ascii (format "[%s] <denote:%s>" desc path))
|
||
('md (format "[%s](%s)" desc path))
|
||
(_ path))
|
||
(format-message "[[Denote query for `%s']]" query))))
|
||
|
||
(defun denote-link-ol-help-echo (_window _object position)
|
||
"Echo the full file path of the identifier at POSITION."
|
||
(let* ((data (denote--link-at-point-get-data position))
|
||
(target (caar data)))
|
||
(denote-get-path-by-id target)))
|
||
|
||
(declare-function org-link-preview-file "ol" (ov path link))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-preview-file (overlay link-target link-data)
|
||
"Use `org-link-preview-file' for OVERLAY, LINK-TARGET, and LINK-DATA.
|
||
Unless the LINK-TARGET has search options, then try to produce a preview.
|
||
|
||
For more details, refer to the documentation of `org-link-set-parameters'."
|
||
(pcase-let* ((`(,path ,_ ,file-search) (denote-link--ol-resolve-link-to-target link-target :full-data)))
|
||
(unless file-search
|
||
(org-link-preview-file overlay path link-data))))
|
||
|
||
;; The `eval-after-load' part with the quoted lambda is adapted from
|
||
;; Elfeed: <https://github.com/skeeto/elfeed/>.
|
||
;;;###autoload
|
||
(eval-after-load 'org
|
||
`(funcall
|
||
;; The extra quote below is necessary because uncompiled closures
|
||
;; do not evaluate to themselves. The quote is harmless for
|
||
;; byte-compiled function objects.
|
||
',(lambda ()
|
||
(with-no-warnings
|
||
(org-link-set-parameters
|
||
"denote"
|
||
:preview #'denote-link-preview-file
|
||
:follow #'denote-link-ol-follow
|
||
:face #'denote-get-link-face
|
||
:help-echo #'denote-link-ol-help-echo
|
||
:complete #'denote-link-ol-complete
|
||
:store #'denote-link-ol-store
|
||
:export #'denote-link-ol-export)))))
|
||
|
||
;;;; Glue code for org-capture
|
||
|
||
(defgroup denote-org-capture ()
|
||
"Integration between Denote and Org Capture."
|
||
:group 'denote)
|
||
|
||
(defcustom denote-org-capture-specifiers "%l\n%i\n%?"
|
||
"String with format specifiers for `org-capture-templates'.
|
||
Check that variable's documentation for the details.
|
||
|
||
The string can include arbitrary text. It is appended to new
|
||
notes via the `denote-org-capture' function. Every new note has
|
||
the standard front matter we define."
|
||
:type 'string
|
||
:package-version '(denote . "0.1.0")
|
||
:group 'denote-org-capture)
|
||
|
||
(defun denote--org-capture-link-specifiers-p ()
|
||
"Return non-nil if `denote-org-capture-specifiers' has link specifiers."
|
||
(when (stringp denote-org-capture-specifiers)
|
||
(string-match-p "%^?[aAlL]" denote-org-capture-specifiers)))
|
||
|
||
(defvar denote-last-path nil "Store last path.")
|
||
|
||
;;;###autoload
|
||
(defun denote-org-capture ()
|
||
"Create new note through `org-capture-templates'.
|
||
Use this as a function that returns the path to the new file.
|
||
The file is populated with Denote's front matter. It can then be
|
||
expanded with the usual specifiers or strings that
|
||
`org-capture-templates' supports.
|
||
|
||
This function obeys `denote-prompts', but it ignores `file-type',
|
||
if present: it always sets the Org file extension for the created
|
||
note to ensure that the capture process works as intended,
|
||
especially for the desired output of the
|
||
`denote-org-capture-specifiers' (which can include arbitrary
|
||
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 ,identifier ,template ,signature)
|
||
(denote--creation-get-note-data-from-prompts))
|
||
(`(,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 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)
|
||
(concat front-matter template-string denote-org-capture-specifiers)))
|
||
|
||
;; TODO 2023-12-02: Maybe simplify `denote-org-capture-with-prompts'
|
||
;; by passing a single PROMPTS that is the same value as `denote-prompts'?
|
||
|
||
;;;###autoload
|
||
(defun denote-org-capture-with-prompts (&optional title keywords subdirectory date template signature)
|
||
"Like `denote-org-capture' but with optional prompt parameters.
|
||
|
||
When called without arguments, do not prompt for anything. Just
|
||
return the front matter with title and keyword fields empty and
|
||
the date and identifier fields specified. Also make the file
|
||
name consist of only the identifier plus the Org file name
|
||
extension.
|
||
|
||
Otherwise produce a minibuffer prompt for every non-nil value that
|
||
corresponds to the TITLE, KEYWORDS, SUBDIRECTORY, DATE, TEMPLATE,
|
||
SIGNATURE arguments. The prompts are those used by the standard
|
||
`denote' command and all of its utility commands.
|
||
|
||
When returning the contents that fill in the Org capture
|
||
template, the sequence is as follows: front matter, TEMPLATE, and
|
||
then the value of the user option `denote-org-capture-specifiers'.
|
||
|
||
Important note: in the case of SUBDIRECTORY actual subdirectories
|
||
must exist---Denote does not create them. Same principle for
|
||
TEMPLATE as templates must exist and are specified in the user
|
||
option `denote-templates'."
|
||
(let ((denote-prompts '()))
|
||
(when signature (push 'signature denote-prompts))
|
||
(when template (push 'template denote-prompts))
|
||
(when date (push 'date denote-prompts))
|
||
(when subdirectory (push 'subdirectory denote-prompts))
|
||
(when keywords (push 'keywords denote-prompts))
|
||
(when title (push 'title denote-prompts))
|
||
(denote-org-capture)))
|
||
|
||
(defun denote-org-capture-delete-empty-file ()
|
||
"Delete file if capture with `denote-org-capture' is aborted."
|
||
(when-let* ((file denote-last-path)
|
||
((denote--file-empty-p file)))
|
||
(delete-file denote-last-path)))
|
||
|
||
(add-hook 'org-capture-after-finalize-hook #'denote-org-capture-delete-empty-file)
|
||
|
||
;;;; The `denote-rename-buffer-mode'
|
||
|
||
(defgroup denote-rename-buffer nil
|
||
"Rename Denote buffers to be shorter and easier to read."
|
||
:group 'denote
|
||
:link '(info-link "(denote) Top")
|
||
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
|
||
|
||
(defvaralias 'denote-buffer-has-backlinks-string 'denote-rename-buffer-backlinks-indicator
|
||
"Alias for `denote-rename-buffer-backlinks-indicator'.")
|
||
|
||
(defcustom denote-rename-buffer-backlinks-indicator " <-->"
|
||
"A string used to indicate that a buffer has backlinks pointing to it."
|
||
:type 'string
|
||
:package-version '(denote . "3.1.0")
|
||
:group 'denote-rename-buffer)
|
||
|
||
(defcustom denote-rename-buffer-format "%D"
|
||
"The format of the buffer name `denote-rename-buffer' should use.
|
||
This also covers the `denote-rename-buffer-mode'. The resulting buffer
|
||
name will also include the `denote-buffer-name-prefix'.
|
||
|
||
The value of this user option is a string that treats specially the
|
||
following specifiers:
|
||
|
||
- The %t is the Denote TITLE in the front matter or the file name.
|
||
- The %T is the Denote TITLE in the file name.
|
||
- The %i is the Denote IDENTIFIER of the file.
|
||
- The %I is the identifier converted to DAYNAME, DAYNUM MONTHNUM YEAR.
|
||
- The %d is the same as %i (DATE mnemonic).
|
||
- The %D is a \"do what I mean\" which behaves the same as %t and if
|
||
that returns nothing, it falls back to %I, then %i.
|
||
- The %s is the Denote SIGNATURE of the file.
|
||
- The %k is the Denote KEYWORDS of the file.
|
||
- The %b inserts `denote-rename-buffer-backlinks-indicator'.
|
||
- The %% is a literal percent sign.
|
||
|
||
In addition, the following flags are available for each of the specifiers:
|
||
|
||
- 0 :: Pad to the width, if given, with zeros instead of spaces.
|
||
- - :: Pad to the width, if given, on the right instead of the left.
|
||
- < :: Truncate to the width and precision, if given, on the left.
|
||
- > :: Truncate to the width and precision, if given, on the right.
|
||
- ^ :: Convert to upper case.
|
||
- _ :: Convert to lower case.
|
||
|
||
When combined all together, the above are written thus:
|
||
|
||
%<flags><width><precision>SPECIFIER-CHARACTER
|
||
|
||
Any other string it taken as-is."
|
||
:type 'string
|
||
:package-version '(denote . "4.0.0")
|
||
:group 'denote-rename-buffer)
|
||
|
||
(defcustom denote-rename-buffer-function #'denote-rename-buffer
|
||
"Symbol of function that is called to rename the Denote file buffer.
|
||
The default `denote-rename-buffer' function uses the pattern
|
||
described in `denote-rename-buffer-format'.
|
||
|
||
Users can set this variable to an arbitrary function that does
|
||
something else. The function is called without arguments from
|
||
the `find-file-hook' and `denote-after-new-note-hook'.
|
||
|
||
A nil value for this variable means that the title of the Denote
|
||
buffer will be used, if available."
|
||
:type '(choice
|
||
(const :tag "Rename using the `denote-rename-buffer-format'" denote-rename-buffer)
|
||
(function :tag "Use a custom renaming function"))
|
||
:package-version '(denote . "2.1.0")
|
||
:group 'denote-rename-buffer)
|
||
|
||
(defun denote-rename-buffer--format (buffer)
|
||
"Parse the BUFFER through the `denote-rename-buffer-format'."
|
||
(when-let* ((file (buffer-file-name buffer)))
|
||
(let ((type (denote-filetype-heuristics file))
|
||
(should-show-backlink-indicator (and ; only do search if format contains "%b"
|
||
(string-match-p "%b" denote-rename-buffer-format)
|
||
(denote--file-has-backlinks-p file))))
|
||
(string-trim
|
||
(format-spec denote-rename-buffer-format
|
||
(list (cons ?t (cond
|
||
((denote-retrieve-front-matter-title-value file type))
|
||
((denote-retrieve-filename-title file))
|
||
(t "")))
|
||
(cons ?T (or (denote-retrieve-filename-title file) ""))
|
||
(cons ?b (if should-show-backlink-indicator denote-rename-buffer-backlinks-indicator ""))
|
||
(cons ?i (or (denote-retrieve-filename-identifier file) ""))
|
||
;; 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-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) ""))
|
||
(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-date-identifier-p id)
|
||
(format-time-string "%A, %e %B %Y" (date-to-time (denote-id-to-date id)))
|
||
id)))
|
||
(t "")))
|
||
(cons ?s (or (denote-retrieve-filename-signature file) ""))
|
||
(cons ?k (or (denote-retrieve-filename-keywords file) ""))
|
||
(cons ?% "%"))
|
||
'delete)))))
|
||
|
||
(defun denote-rename-buffer (&optional buffer)
|
||
"Rename current buffer or optional BUFFER with `denote-rename-buffer-format'.
|
||
The symbol of this function is the default value of the user
|
||
option `denote-rename-buffer-function' and is thus used by the
|
||
`denote-rename-buffer-mode'."
|
||
(when-let* ((file (buffer-file-name buffer))
|
||
((denote-file-has-identifier-p file))
|
||
(new-name (denote-rename-buffer--format (or buffer (current-buffer))))
|
||
((not (string-blank-p new-name))))
|
||
(rename-buffer (denote-format-buffer-name new-name) :unique)))
|
||
|
||
(defun denote-rename-buffer--fallback (&optional buffer)
|
||
"Fallback to rename BUFFER or `current-buffer'.
|
||
This is called if `denote-rename-buffer-rename-function' is nil."
|
||
(let ((denote-rename-buffer-format "%t"))
|
||
(denote-rename-buffer buffer)))
|
||
|
||
(defun denote-rename-buffer-rename-function-or-fallback ()
|
||
"Call `denote-rename-buffer-function' or its fallback to rename with title.
|
||
Add this to `find-file-hook' and `denote-after-new-note-hook'."
|
||
(when-let* ((file (buffer-file-name))
|
||
((denote-file-has-identifier-p file)))
|
||
(funcall (or denote-rename-buffer-function #'denote-rename-buffer--fallback))))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode denote-rename-buffer-mode
|
||
"Automatically rename Denote buffers to be easier to read.
|
||
A buffer is renamed upon visiting the underlying file. This
|
||
means that existing buffers are not renamed until they are
|
||
visited again in a new buffer (files are visited with the command
|
||
`find-file' or related)."
|
||
:global t
|
||
(if denote-rename-buffer-mode
|
||
(progn
|
||
(add-hook 'denote-after-new-note-hook #'denote-rename-buffer-rename-function-or-fallback)
|
||
(add-hook 'denote-after-rename-file-hook #'denote-rename-buffer-rename-function-or-fallback)
|
||
(add-hook 'find-file-hook #'denote-rename-buffer-rename-function-or-fallback))
|
||
(remove-hook 'denote-after-new-note-hook #'denote-rename-buffer-rename-function-or-fallback)
|
||
(remove-hook 'denote-after-rename-file-hook #'denote-rename-buffer-rename-function-or-fallback)
|
||
(remove-hook 'find-file-hook #'denote-rename-buffer-rename-function-or-fallback)))
|
||
|
||
(provide 'denote)
|
||
;;; denote.el ends here
|