mirror of
https://github.com/protesilaos/denote.git
synced 2026-09-10 07:16:20 -04:00
4168 lines
166 KiB
EmacsLisp
4168 lines
166 KiB
EmacsLisp
;;; denote.el --- Simple notes with an efficient file-naming scheme -*- lexical-binding: t -*-
|
||
|
||
;; Copyright (C) 2022-2023 Free Software Foundation, Inc.
|
||
|
||
;; Author: Protesilaos Stavrou <info@protesilaos.com>
|
||
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
|
||
;; URL: https://git.sr.ht/~protesilaos/denote
|
||
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
|
||
;; Version: 2.1.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 aims to be a simple-to-use, focused-in-scope, and effective
|
||
;; note-taking and file-naming tool for Emacs.
|
||
;;
|
||
;; Denote is based on the idea that files should follow a predictable
|
||
;; and descriptive file-naming scheme. The file name must offer a
|
||
;; clear indication of what the contents are about, without reference
|
||
;; to any other metadata. Denote basically streamlines the creation
|
||
;; of such files or file names while providing facilities to link
|
||
;; between them (where those files are editable).
|
||
;;
|
||
;; 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 constistent way makes their
|
||
;; filtering and retrieval considerably easier. Denote provides relevant
|
||
;; facilities to rename files, regardless of file type.
|
||
;;
|
||
;; The manual describes all the technicalities about the file-naming
|
||
;; scheme, points of entry to creating new notes, commands to check
|
||
;; links between notes, and more: ;; <https://protesilaos.com/emacs/denote>.
|
||
;; If you have the info manual available, evaluate:
|
||
;;
|
||
;; (info "(denote) Top")
|
||
;;
|
||
;; What follows is a general overview of its core core design
|
||
;; principles (again: please read the manual for the technicalities):
|
||
;;
|
||
;; * Predictability :: File names must follow a consistent and
|
||
;; descriptive naming convention (see the manual's "The file-naming
|
||
;; scheme"). The file name alone should offer a clear indication of
|
||
;; what the contents are, without reference to any other metadatum.
|
||
;; This convention is not specific to note-taking, as it is pertinent
|
||
;; to any form of file that is part of the user's long-term storage
|
||
;; (see the manual's "Renaming files").
|
||
;;
|
||
;; * Composability :: Be a good Emacs citizen, by integrating with other
|
||
;; packages or built-in functionality instead of re-inventing
|
||
;; functions such as for filtering or greping. The author of Denote
|
||
;; (Protesilaos, aka "Prot") writes ordinary notes in plain text
|
||
;; (`.txt'), switching on demand to an Org file only when its expanded
|
||
;; set of functionality is required for the task at hand (see the
|
||
;; manual's "Points of entry").
|
||
;;
|
||
;; * Portability :: Notes are plain text and should remain portable.
|
||
;; The way Denote writes file names, the front matter it includes in
|
||
;; the note's header, and the links it establishes must all be
|
||
;; adequately usable with standard Unix tools. No need for a databse
|
||
;; or some specialised software. As Denote develops and this manual
|
||
;; is fully fleshed out, there will be concrete examples on how to do
|
||
;; the Denote-equivalent on the command-line.
|
||
;;
|
||
;; * Flexibility :: Do not assume the user's preference for a
|
||
;; note-taking methodology. Denote is conceptually similar to the
|
||
;; Zettelkasten Method, which you can learn more about in this
|
||
;; detailed introduction: <https://zettelkasten.de/introduction/>.
|
||
;; Notes are atomic (one file per note) and have a unique identifier.
|
||
;; However, Denote does not enforce a particular methodology for
|
||
;; knowledge management, such as a restricted vocabulary or mutually
|
||
;; exclusive sets of keywords. Denote also does not check if the user
|
||
;; writes thematically atomic notes. It is up to the user to apply
|
||
;; the requisite rigor and/or creativity in pursuit of their preferred
|
||
;; workflow (see the manual's "Writing metanotes").
|
||
;;
|
||
;; * Hackability :: Denote's code base consists of small and reusable
|
||
;; functions. They all have documentation strings. The idea is to
|
||
;; make it easier for users of varying levels of expertise to
|
||
;; understand what is going on and make surgical interventions where
|
||
;; necessary (e.g. to tweak some formatting). In this manual, we
|
||
;; provide concrete examples on such user-level configurations (see
|
||
;; the manual's "Keep a journal or diary").
|
||
;;
|
||
;; Now the important part... "Denote" is the familiar word, though it
|
||
;; also is a play on the "note" concept. Plus, we can come up with
|
||
;; acronyms, recursive or otherwise, of increasingly dubious utility
|
||
;; like:
|
||
;;
|
||
;; + Don't Ever Note Only The Epiphenomenal
|
||
;; + Denote Everything Neatly; Omit The Excesses
|
||
;;
|
||
;; But we'll let you get back to work. Don't Eschew or Neglect your
|
||
;; Obligations, Tasks, and Engagements.
|
||
|
||
;;; 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 (eq val 'local) (eq val 'default-directory))))
|
||
(defcustom denote-directory (expand-file-name "~/Documents/notes/")
|
||
"Directory for storing personal notes.
|
||
|
||
A safe local value of either `default-directory' or `local' can
|
||
be added as a value in a .dir-local.el file. Do this if you
|
||
intend to use multiple directory silos for your notes while still
|
||
relying on a global value (which is the value of this variable).
|
||
The Denote manual has a sample (search for '.dir-locals.el').
|
||
Those silos do not communicate with each other: they remain
|
||
separate.
|
||
|
||
The local value influences where commands such as `denote' will
|
||
place the newly created note. If the command is called from a
|
||
directory or file where the local value exists, then that value
|
||
take precedence, otherwise the global value is used.
|
||
|
||
If you intend to reference this variable in Lisp, consider using
|
||
the function `denote-directory' instead: it returns the path as a
|
||
directory and also checks if a safe local value should be used."
|
||
:group 'denote
|
||
:safe (lambda (val) (or (eq val 'local) (eq val 'default-directory)))
|
||
:package-version '(denote . "2.0.0")
|
||
:link '(info-link "(denote) Maintain separate directories for notes")
|
||
:type 'directory)
|
||
|
||
(defcustom denote-known-keywords
|
||
'("emacs" "philosophy" "politics" "economics")
|
||
"List of strings with predefined keywords for `denote'.
|
||
Also see user options: `denote-infer-keywords',
|
||
`denote-sort-keywords', `denote-file-name-letter-casing'."
|
||
:group 'denote
|
||
:package-version '(denote . "0.1.0")
|
||
:type '(repeat string))
|
||
|
||
(defcustom denote-infer-keywords t
|
||
"Whether to infer keywords from existing notes' file names.
|
||
|
||
When non-nil, 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. The aforementioned
|
||
completion prompt only shows the `denote-known-keywords'. Use
|
||
this if you want to enforce a restricted vocabulary.
|
||
|
||
The user option `denote-excluded-keywords-regexp' can be used to
|
||
exclude keywords that match a regular expression.
|
||
|
||
Inferred keywords are specific to the value of the variable
|
||
`denote-directory'. If a silo with a local value is used, as
|
||
explained in that variable's doc string, the inferred keywords
|
||
are specific to the given silo.
|
||
|
||
For advanced Lisp usage, the function `denote-keywords' returns
|
||
the appropriate list of strings."
|
||
:group 'denote
|
||
:package-version '(denote . "0.1.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-prompts '(title keywords)
|
||
"Specify the prompts of the `denote' command for interactive use.
|
||
|
||
The value 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 `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'.)
|
||
|
||
- `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
|
||
to establish a sequential relationship between files (e.g. 1,
|
||
1a, 1b, 1b1, 1b2, ...). Signatures have no strictly defined
|
||
function and are up to the user to apply as they see fit. One
|
||
use-case is to implement Niklas Luhmann's Zettelkasten system
|
||
for a sequence of notes (Folgezettel). Signatures are not
|
||
included in a file's front matter. They are reserved solely
|
||
for creating a sequence in a file listing, at least for the
|
||
time being. To insert a link that includes the signature, use
|
||
the command `denote-link-with-signature'.
|
||
|
||
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
|
||
`denote-file-type').
|
||
|
||
Recall that Denote's standard file-naming scheme is defined as
|
||
follows (read the manual for the technicalities):
|
||
|
||
DATE--TITLE__KEYWORDS.EXT
|
||
|
||
Depending on the inclusion of the `title', `keywords', and
|
||
`signature' prompts, file names will be any of those
|
||
permutations:
|
||
|
||
DATE.EXT
|
||
DATE--TITLE.EXT
|
||
DATE__KEYWORDS.EXT
|
||
DATE==SIGNATURE.EXT
|
||
DATE==SIGNATURE--TITLE.EXT
|
||
DATE==SIGNATURE--TITLE__KEYWORDS.EXT
|
||
DATE==SIGNATURE__KEYWORDS.EXT
|
||
|
||
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' command (advanced users can call it from Lisp). For
|
||
ad-hoc interactive actions that do not change the default
|
||
behaviour of the `denote' command, users can invoke these
|
||
convenience commands: `denote-type', `denote-subdirectory',
|
||
`denote-date', `denote-template', `denote-signature'."
|
||
:group 'denote
|
||
:package-version '(denote . "2.0.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 "File type extension" file-type)
|
||
(const :tag "Subdirectory" subdirectory)
|
||
(const :tag "Template" template)
|
||
(const :tag "Signature" signature))))
|
||
|
||
(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)
|
||
|
||
(make-obsolete
|
||
'denote-allow-multi-word-keywords
|
||
'denote-file-name-letter-casing
|
||
"2.1.0")
|
||
|
||
(defcustom denote-file-type nil
|
||
"The file type extension for new notes.
|
||
|
||
By default (a nil value), the file type is that of Org mode.
|
||
Though the `org' symbol can be specified for the same effect.
|
||
|
||
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 non-nil value is the same as the default.
|
||
|
||
NOTE: expert users can change the supported file types by leaving
|
||
the value of this user option to nil and directly 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 . "0.6.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 `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 doc string 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-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 . STRING) association.
|
||
|
||
- The KEY is the name which identifies the template. It is an
|
||
arbitrary symbol, such as `report', `memo', `statement'.
|
||
|
||
- The STRING 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.
|
||
|
||
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 string)
|
||
:package-version '(denote . "0.5.0")
|
||
:link '(info-link "(denote) The denote-templates option")
|
||
:group 'denote)
|
||
|
||
(defcustom denote-backlinks-show-context nil
|
||
"When non-nil, show link context in the backlinks buffer.
|
||
|
||
The context is the line a link to the current note is found in.
|
||
The context includes multiple links to the same note, if those
|
||
are present.
|
||
|
||
When nil, only show a simple list of file names that link to the
|
||
current note."
|
||
:group 'denote
|
||
:package-version '(denote . "1.2.0")
|
||
:type 'boolean)
|
||
|
||
(defcustom denote-rename-no-confirm nil
|
||
"When non-nil, `denote-rename-file' does not prompt for confirmation.
|
||
The default behaviour of the `denote-rename-file' command 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.
|
||
|
||
Remember that `denote-rename-file' does not save the underlying
|
||
buffer it modifies. It leaves it unsaved so that the user can
|
||
review what happened, such as by invoking the command
|
||
`diff-buffer-with-file'.
|
||
|
||
Specialised commands that build on top of `denote-rename-file'
|
||
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 (buffers remain unsaved))."
|
||
:group 'denote
|
||
:package-version '(denote . "2.1.0")
|
||
:type 'boolean)
|
||
|
||
(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 'string)
|
||
|
||
(defcustom denote-excluded-keywords-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 . "1.2.0")
|
||
:type 'string)
|
||
|
||
(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")
|
||
: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)
|
||
|
||
(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-file-name-letter-casing
|
||
'((title . downcase)
|
||
(signature . downcase)
|
||
(keywords . downcase)
|
||
(t . downcase))
|
||
"Specify the method Denote uses to affect the letter casing of file names.
|
||
|
||
The value is an alist where each element is a cons cell of the
|
||
form (COMPONENT . METHOD).
|
||
|
||
- The COMPONENT is an unquoted symbol among `title', `signature',
|
||
`keywords', which refers to the corresponding component of the
|
||
file name. The special t COMPONENT is a fallback value in case
|
||
the others are not specified.
|
||
|
||
- The METHOD is the letter casing scheme, which is an unquoted
|
||
symbol of either `downcase' or `verbatim'. A nil value has the
|
||
same meaning as `downcase'. Other non-nil METHOD types are
|
||
reserved for possible future use.
|
||
|
||
The `downcase' METHOD converts user input for the given
|
||
COMPONENT into lower case. The benefit of this approach (which
|
||
is the default behaviour) is that file names remain consistent
|
||
over the long-term. The user never needs to account for
|
||
varying letter casing while working with them.
|
||
|
||
The `verbatim' METHOD means that Denote will not affect the
|
||
letter casing of user input when generating the given file name
|
||
COMPONENT. As such, conventions like CamelCase or camelCase
|
||
are respected. The user thus assumes responsibility to keep
|
||
file names in a good state over the long term."
|
||
:group 'denote
|
||
:type '(alist
|
||
:key (choice :tag "File name component"
|
||
(const :tag "The --TITLE component of the file name" title)
|
||
(const :tag "The ==SIGNATURE component of the file name" signature)
|
||
(const :tag "The __KEYWORDS component of the file name" keywords)
|
||
(const :tag "Fallback for any unspecified file name component" t))
|
||
:value (choice :tag "Letter casing method"
|
||
(const :tag "Downcase file names (default)" downcase)
|
||
(const :tag "Accept file name inputs verbatim" verbatim)))
|
||
:link '(info-link "(denote) Contol the letter casing of file names")
|
||
:package-version '(denote . "2.1.0"))
|
||
|
||
;;;; Main variables
|
||
|
||
;; For character classes, evaluate: (info "(elisp) Char Classes")
|
||
|
||
(defconst denote-id-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.")
|
||
|
||
(defconst denote-id-regexp "\\([0-9]\\{8\\}\\)\\(T[0-9]\\{6\\}\\)"
|
||
"Regular expression to match `denote-id-format'.")
|
||
|
||
(defconst denote-signature-regexp "==\\([[:alnum:][:nonascii:]=]*\\)"
|
||
"Regular expression to match the SIGNATURE field in a file name.")
|
||
|
||
(defconst denote-title-regexp "--\\([[:alnum:][:nonascii:]-]*\\)"
|
||
"Regular expression to match the TITLE field in a file name.")
|
||
|
||
(defconst denote-keywords-regexp "__\\([[:alnum:][:nonascii:]_-]*\\)"
|
||
"Regular expression to match the KEYWORDS field in a file name.")
|
||
|
||
(defconst denote-excluded-punctuation-regexp "[][{}!@#$%^&*()=+'\"?,.\|;:~`‘’“”/]*"
|
||
"Punctionation that is removed from file names.
|
||
We consider those characters illegal for our purposes.")
|
||
|
||
(defvar denote-excluded-punctuation-extra-regexp nil
|
||
"Additional punctuation that is removed from file names.
|
||
This variable is for advanced users who need to extend the
|
||
`denote-excluded-punctuation-regexp'. Once we have a better
|
||
understanding of what we should be omitting, we will update
|
||
things accordingly.")
|
||
|
||
;;;; File helper functions
|
||
|
||
(defun denote--completion-table (category candidates)
|
||
"Pass appropriate metadata CATEGORY to completion CANDIDATES."
|
||
(lambda (string pred action)
|
||
(if (eq action 'metadata)
|
||
`(metadata (category . ,category))
|
||
(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)
|
||
(t nil))))
|
||
|
||
(defun denote--make-denote-directory ()
|
||
"Make the variable `denote-directory' and its parents, if needed."
|
||
(when (and (stringp denote-directory)
|
||
(not (file-directory-p denote-directory)))
|
||
(make-directory denote-directory :parents)))
|
||
|
||
(defvar denote-user-enforced-denote-directory nil
|
||
"Value of the variable `denote-directory'.
|
||
Use this to `let' bind a directory path, thus overriding what the
|
||
function `denote-directory' ordinarily returns.")
|
||
|
||
(defun denote-directory ()
|
||
"Return path of variable `denote-directory' as a proper directory.
|
||
Custom Lisp code can `let' bind the value of the variable
|
||
`denote-user-enforced-denote-directory' to override what this
|
||
function returns.
|
||
|
||
Otherwise, the order of precedence is to first check for a silo
|
||
before falling back to the value of the variable
|
||
`denote-directory'."
|
||
(let ((path (or denote-user-enforced-denote-directory
|
||
(denote--default-directory-is-silo-p)
|
||
(denote--make-denote-directory)
|
||
(default-value 'denote-directory))))
|
||
(file-name-as-directory (expand-file-name path))))
|
||
|
||
(defun denote--slug-no-punct (str &optional extra-characters)
|
||
"Remove punctuation from STR.
|
||
Concretely, replace with spaces anything that matches the
|
||
`denote-excluded-punctuation-regexp' and
|
||
`denote-excluded-punctuation-extra-regexp'.
|
||
|
||
With optional EXTRA-CHARACTERS as a string, include them in the
|
||
regexp to be replaced."
|
||
(replace-regexp-in-string
|
||
(concat denote-excluded-punctuation-regexp
|
||
denote-excluded-punctuation-extra-regexp
|
||
extra-characters)
|
||
"" str))
|
||
|
||
(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-letter-case (component args)
|
||
"Apply letter casing specified by COMPONENT to ARGS.
|
||
COMPONENT is a symbol representing a file name component, as
|
||
described in the user option `denote-file-name-letter-casing'."
|
||
(if (or (eq (alist-get component denote-file-name-letter-casing) 'verbatim)
|
||
(eq (alist-get t denote-file-name-letter-casing) 'verbatim))
|
||
args
|
||
(funcall #'downcase args)))
|
||
|
||
(defun denote-sluggify (str &optional component)
|
||
"Make STR an appropriate slug for file name COMPONENT.
|
||
|
||
COMPONENT is a symbol used to retrieve the letter casing method
|
||
corresponding to the file name field is references. COMPONENT is
|
||
described in the user option `denote-file-name-letter-casing'.
|
||
|
||
A nil value of COMPONENT has the same meaning as applying
|
||
`downcase' to STR."
|
||
(denote-letter-case component (denote--slug-hyphenate (denote--slug-no-punct 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-sluggify-signature (str)
|
||
"Make STR an appropriate slug for signatures.
|
||
Perform letter casing according to `denote-file-name-letter-casing'."
|
||
(denote-letter-case 'signature (denote--slug-put-equals (denote--slug-no-punct str "-"))))
|
||
|
||
(defun denote-sluggify-and-join (str)
|
||
"Sluggify STR while joining separate words."
|
||
(denote-letter-case
|
||
'keywords
|
||
(replace-regexp-in-string
|
||
"-" ""
|
||
(denote--slug-hyphenate (denote--slug-no-punct str)))))
|
||
|
||
(defun denote-sluggify-keywords (keywords)
|
||
"Sluggify KEYWORDS, which is a list of strings."
|
||
(if (listp keywords)
|
||
(mapcar #'denote-sluggify-and-join keywords)
|
||
(error "`%s' is not a list" keywords)))
|
||
|
||
;; TODO 2023-05-22: Review name of `denote-desluggify' to signify what
|
||
;; the doc string warns about.
|
||
(defun denote-desluggify (str)
|
||
"Upcase first char in STR and dehyphenate STR, inverting `denote-sluggify'.
|
||
The intent of this function is to be used on individual strings,
|
||
such as the TITLE component of a Denote file name, but not on the
|
||
entire file name. Put differently, it does not work with
|
||
signatures and keywords."
|
||
(let ((str (replace-regexp-in-string "-" " " str)))
|
||
(aset str 0 (upcase (aref str 0)))
|
||
str))
|
||
|
||
(defun denote--file-empty-p (file)
|
||
"Return non-nil if FILE is empty."
|
||
(zerop (or (file-attribute-size (file-attributes file)) 0)))
|
||
|
||
(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 `denote-file-type'."
|
||
(seq-some (lambda (e)
|
||
(string-suffix-p e file))
|
||
(denote-file-type-extensions-with-encryption)))
|
||
|
||
(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 `denote-file-type'."
|
||
(and (string-prefix-p (denote-directory) (expand-file-name filename))
|
||
(string-match-p (concat "\\`" denote-id-regexp)
|
||
(file-name-nondirectory 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 not be a directory, must satisfy
|
||
`file-regular-p' and `denote-filename-is-note-p'."
|
||
(and (not (file-directory-p file))
|
||
(file-regular-p file)
|
||
(denote-filename-is-note-p file)))
|
||
|
||
(defun denote-file-has-identifier-p (file)
|
||
"Return non-nil if FILE has a Denote identifier."
|
||
(when file
|
||
(string-match-p (concat "\\`" denote-id-regexp)
|
||
(file-name-nondirectory file))))
|
||
|
||
(defun denote-file-has-signature-p (file)
|
||
"Return non-nil if FILE has a Denote identifier."
|
||
(when file
|
||
(string-match-p denote-signature-regexp
|
||
(file-name-nondirectory file))))
|
||
|
||
(make-obsolete 'denote-file-directory-p nil "2.0.0")
|
||
|
||
(defun denote--file-regular-writable-p (file)
|
||
"Return non-nil if FILE is regular and writable."
|
||
(and (file-regular-p file)
|
||
(file-writable-p file)))
|
||
|
||
(defun denote-file-is-writable-and-supported-p (file)
|
||
"Return non-nil if FILE is writable and has supported extension."
|
||
(and (denote--file-regular-writable-p file)
|
||
(denote-file-has-supported-extension-p file)))
|
||
|
||
(defun denote-get-file-name-relative-to-denote-directory (file)
|
||
"Return name of FILE relative to the variable `denote-directory'.
|
||
FILE must be an absolute path."
|
||
(when-let ((dir (denote-directory))
|
||
((file-name-absolute-p file))
|
||
(file-name (expand-file-name file))
|
||
((string-prefix-p dir file-name)))
|
||
(substring-no-properties file-name (length dir))))
|
||
|
||
(defun denote-extract-id-from-string (string)
|
||
"Return existing Denote identifier in STRING, else nil."
|
||
(when (string-match denote-id-regexp string)
|
||
(match-string 0 string)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--default-dir-has-denote-prefix
|
||
'denote--dir-in-denote-directory-p
|
||
"2.1.0")
|
||
|
||
(defun denote--exclude-directory-regexp-p (file)
|
||
"Return non-nil if FILE matches `denote-excluded-directories-regexp'."
|
||
(and 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-directory'.
|
||
Avoids traversing dotfiles (unconditionally) and whatever matches
|
||
`denote-excluded-directories-regexp'."
|
||
(directory-files-recursively
|
||
(denote-directory)
|
||
directory-files-no-dot-files-regexp
|
||
:include-directories
|
||
(lambda (f)
|
||
(cond
|
||
((string-match-p "\\`\\." f) nil)
|
||
((string-match-p "/\\." f) nil)
|
||
((denote--exclude-directory-regexp-p f) nil)
|
||
((file-readable-p f))
|
||
(t)))
|
||
:follow-symlinks))
|
||
|
||
(defun denote-directory-files ()
|
||
"Return list of absolute file paths in variable `denote-directory'.
|
||
|
||
Files only need to have an identifier. The return value may thus
|
||
include file types that are not implied by `denote-file-type'.
|
||
To limit the return value to text files, use the function
|
||
`denote-directory-text-only-files'.
|
||
|
||
Remember that the variable `denote-directory' accepts a dir-local
|
||
value, as explained in its doc string."
|
||
(mapcar
|
||
#'expand-file-name
|
||
(seq-filter
|
||
#'denote-file-has-identifier-p
|
||
(denote--directory-all-files-recursively))))
|
||
|
||
(defun denote-directory-text-only-files ()
|
||
"Return list of text files in variable `denote-directory'.
|
||
Filter `denote-directory-files' using `denote-file-is-note-p'."
|
||
(seq-filter #'denote-file-is-note-p (denote-directory-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'."
|
||
(seq-remove
|
||
(lambda (filename)
|
||
(let ((rel (denote-get-file-name-relative-to-denote-directory filename)))
|
||
(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)))
|
||
|
||
(define-obsolete-variable-alias
|
||
'denote--encryption-file-extensions
|
||
'denote-encryption-file-extensions
|
||
"2.0.0")
|
||
|
||
;; 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.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--extensions-with-encryption
|
||
'denote-file-type-extensions-with-encryption
|
||
"2.0.0")
|
||
|
||
(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)
|
||
(and (denote-file-has-identifier-p file)
|
||
(string-prefix-p id (file-name-nondirectory file))))
|
||
(denote-directory-files))))
|
||
(if (length< files 2)
|
||
(car files)
|
||
(seq-find
|
||
(lambda (file)
|
||
(let ((file-extension (denote-get-file-extension file)))
|
||
(and (denote-file-is-note-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’)."
|
||
(file-relative-name (denote-get-path-by-id id) directory))
|
||
|
||
(defun denote-directory-files-matching-regexp (regexp)
|
||
"Return list of files matching REGEXP in `denote-directory-files'."
|
||
(seq-filter
|
||
(lambda (f)
|
||
(string-match-p regexp (denote-get-file-name-relative-to-denote-directory f)))
|
||
(denote-directory-files)))
|
||
|
||
(defun denote-all-files (&optional omit-current)
|
||
"Return the list of Denote files in variable `denote-directory'.
|
||
With optional OMIT-CURRENT, do not include the current Denote
|
||
file in the returned list."
|
||
(let ((files (denote-directory-files)))
|
||
(if (and omit-current (denote-file-has-identifier-p buffer-file-name))
|
||
(delete buffer-file-name files)
|
||
files)))
|
||
|
||
(defvar denote--file-history nil
|
||
"Minibuffer history of `denote-file-prompt'.")
|
||
|
||
(defun denote-file-prompt (&optional files-matching-regexp)
|
||
"Prompt for file with identifier in variable `denote-directory'.
|
||
With optional FILES-MATCHING-REGEXP, filter the candidates per
|
||
the given regular expression."
|
||
(let ((files (if files-matching-regexp
|
||
(denote-directory-files-matching-regexp files-matching-regexp)
|
||
(denote-all-files :omit-current))))
|
||
(completing-read "Select note: " files nil nil nil 'denote--file-history)))
|
||
|
||
;;;; 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."
|
||
(let* ((file-name (file-name-nondirectory path))
|
||
(kws (when (string-match denote-keywords-regexp file-name)
|
||
(match-string-no-properties 1 file-name))))
|
||
(when kws
|
||
(split-string kws "_"))))
|
||
|
||
(defun denote--inferred-keywords ()
|
||
"Extract keywords from `denote-directory-files'.
|
||
This function returns duplicates. The `denote-keywords' is the
|
||
one that doesn't."
|
||
(let ((kw (mapcan #'denote-extract-keywords-from-path (denote-directory-files))))
|
||
(if-let ((regexp denote-excluded-keywords-regexp))
|
||
(seq-remove (apply-partially #'string-match-p regexp) kw)
|
||
kw)))
|
||
|
||
(defun denote-keywords ()
|
||
"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.
|
||
|
||
Inferred keywords are filtered by the user option
|
||
`denote-excluded-keywords-regexp'."
|
||
(delete-dups
|
||
(if denote-infer-keywords
|
||
(append (denote--inferred-keywords) denote-known-keywords)
|
||
denote-known-keywords)))
|
||
|
||
(defvar denote--keyword-history nil
|
||
"Minibuffer history of inputted keywords.")
|
||
|
||
(defun denote--keywords-crm (keywords &optional prompt)
|
||
"Use `completing-read-multiple' for KEYWORDS.
|
||
With optional PROMPT, use it instead of a generic text for file
|
||
keywords."
|
||
(delete-dups
|
||
(completing-read-multiple
|
||
(format-prompt (or prompt "File keyword") nil)
|
||
keywords nil nil nil 'denote--keyword-history)))
|
||
|
||
(defun denote-keywords-prompt (&optional prompt-text)
|
||
"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.
|
||
|
||
Process the return value with `denote-keywords-sort' and sort
|
||
with `string-collate-lessp' if the user option
|
||
`denote-sort-keywords' is non-nil."
|
||
(denote-keywords-sort (denote--keywords-crm (denote-keywords) prompt-text)))
|
||
|
||
(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 keywords #'string-collate-lessp)
|
||
keywords))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--keywords-combine
|
||
'denote-keywords-combine
|
||
"2.1.0")
|
||
|
||
(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."
|
||
(mapconcat
|
||
(lambda (k)
|
||
(denote-letter-case 'keywords k))
|
||
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
|
||
\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
|
||
---\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
|
||
+++\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
|
||
---------------------------\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-surround-with-quotes (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."
|
||
(if (string-blank-p s)
|
||
""
|
||
(let ((trims "[ \t\n\r]+"))
|
||
(string-trim s trims trims))))
|
||
|
||
(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."
|
||
(if (string-blank-p s)
|
||
""
|
||
(denote--trim-quotes (denote-trim-whitespace 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. If KEYWORDS-STRING
|
||
satisfies `string-blank-p', return an empty list.
|
||
|
||
Consult the `denote-file-types' for how this is used."
|
||
(if (string-blank-p keywords-string)
|
||
'()
|
||
(split-string keywords-string "[:,\s]+" t "[][ \"']+")))
|
||
|
||
(defvar denote-file-types
|
||
'((org
|
||
:extension ".org"
|
||
:date-function denote-date-org-timestamp
|
||
:front-matter denote-org-front-matter
|
||
:title-key-regexp "^#\\+title\\s-*:"
|
||
:title-value-function identity
|
||
: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
|
||
:link denote-org-link-format
|
||
:link-in-context-regexp denote-org-link-in-context-regexp)
|
||
(markdown-yaml
|
||
:extension ".md"
|
||
:date-function denote-date-rfc3339
|
||
:front-matter denote-yaml-front-matter
|
||
:title-key-regexp "^title\\s-*:"
|
||
:title-value-function denote-surround-with-quotes
|
||
: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
|
||
:link denote-md-link-format
|
||
:link-in-context-regexp denote-md-link-in-context-regexp)
|
||
(markdown-toml
|
||
:extension ".md"
|
||
:date-function denote-date-rfc3339
|
||
:front-matter denote-toml-front-matter
|
||
:title-key-regexp "^title\\s-*="
|
||
:title-value-function denote-surround-with-quotes
|
||
: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
|
||
:link denote-md-link-format
|
||
:link-in-context-regexp denote-md-link-in-context-regexp)
|
||
(text
|
||
:extension ".txt"
|
||
:date-function denote-date-iso-8601
|
||
:front-matter denote-text-front-matter
|
||
:title-key-regexp "^title\\s-*:"
|
||
:title-value-function identity
|
||
: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
|
||
:link denote-org-link-format
|
||
:link-in-context-regexp denote-org-link-in-context-regexp))
|
||
"Alist of `denote-file-type' and their format properties.
|
||
|
||
Each element is of the form (SYMBOL PROPERTY-LIST). SYMBOL is
|
||
one of those specified in `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.
|
||
|
||
- `: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 doc string 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' 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 `denote-file-type' is nil, use the first element of this list
|
||
for new note creation. The default is `org'.")
|
||
|
||
(defun denote--date-format-function (file-type)
|
||
"Return date format function of FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:date-function))
|
||
|
||
(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."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:title-key-regexp))
|
||
|
||
(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))
|
||
|
||
(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."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:keywords-key-regexp))
|
||
|
||
(defun denote--keywords-value-function (file-type)
|
||
"Convert keywords' string 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--link-format (file-type)
|
||
"Return link format extension based on FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:link))
|
||
|
||
(defun denote--link-in-context-regexp (file-type)
|
||
"Return link regexp in context based on FILE-TYPE."
|
||
(plist-get
|
||
(alist-get file-type denote-file-types)
|
||
:link-in-context-regexp))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--extensions
|
||
'denote-file-type-extensions
|
||
"2.0.0")
|
||
|
||
(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-title-line-from-front-matter (title file-type)
|
||
"Retrieve title line from front matter based on FILE-TYPE.
|
||
Format TITLE in the title line. The returned line does not
|
||
contain the newline."
|
||
(let ((front-matter (denote--format-front-matter title "" nil "" file-type))
|
||
(key-regexp (denote--title-key-regexp file-type)))
|
||
(with-temp-buffer
|
||
(insert front-matter)
|
||
(goto-char (point-min))
|
||
(when (re-search-forward key-regexp nil t 1)
|
||
(buffer-substring-no-properties (line-beginning-position) (line-end-position))))))
|
||
|
||
(defun denote--get-keywords-line-from-front-matter (keywords file-type)
|
||
"Retrieve keywords line from front matter based on FILE-TYPE.
|
||
Format KEYWORDS in the keywords line. The returned line does not
|
||
contain the newline."
|
||
(let ((front-matter (denote--format-front-matter "" "" keywords "" file-type))
|
||
(key-regexp (denote--keywords-key-regexp file-type)))
|
||
(with-temp-buffer
|
||
(insert front-matter)
|
||
(goto-char (point-min))
|
||
(when (re-search-forward key-regexp nil t 1)
|
||
(buffer-substring-no-properties (line-beginning-position) (line-end-position))))))
|
||
|
||
;;;; Front matter or content retrieval functions
|
||
|
||
(defun denote-retrieve-filename-identifier (file &optional no-error)
|
||
"Extract identifier from FILE name.
|
||
If NO-ERROR is nil and an identifier is not found, return an
|
||
error, else return nil.
|
||
|
||
To create a new one, refer to the function
|
||
`denote-create-unique-file-identifier'."
|
||
(let ((file-name (file-name-nondirectory file)))
|
||
(if (string-match (concat "\\`" denote-id-regexp) file-name)
|
||
(match-string-no-properties 0 file-name)
|
||
(when (not no-error)
|
||
(error "Cannot find `%s' as a file with a Denote identifier" file)))))
|
||
|
||
(defun denote-create-unique-file-identifier (file used-ids &optional date)
|
||
"Generate a unique identifier for FILE not in USED-IDS hash-table.
|
||
|
||
The conditions are as follows:
|
||
|
||
- If optional DATE is non-nil, invoke
|
||
`denote-prompt-for-date-return-id'.
|
||
|
||
- If DATE is nil, use the file attributes to determine the last
|
||
modified date and format it as an identifier.
|
||
|
||
- As a fallback, derive an identifier from the current time.
|
||
|
||
To only return an existing identifier, refer to the function
|
||
`denote-retrieve-filename-identifier'."
|
||
(let ((id (cond
|
||
(date (denote-prompt-for-date-return-id))
|
||
((denote--file-attributes-time file))
|
||
(t (format-time-string denote-id-format)))))
|
||
(denote--find-first-unused-id id used-ids)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-retrieve-or-create-file-identifier
|
||
'denote-retrieve-filename-identifier
|
||
"2.1.0")
|
||
|
||
(defun denote-retrieve-filename-signature (file)
|
||
"Extract signature from FILE name, if present, else return nil."
|
||
(when (denote-file-has-signature-p file)
|
||
(string-match denote-signature-regexp file)
|
||
(match-string 1 file)))
|
||
|
||
(defun denote-retrieve-filename-title (file)
|
||
"Extract title from FILE name, else return `file-name-base'.
|
||
Run `denote-desluggify' on title if the extraction is sucessful."
|
||
(if-let (((file-exists-p file))
|
||
((denote-file-has-identifier-p file))
|
||
((string-match denote-title-regexp file))
|
||
(title (match-string 1 file)))
|
||
(denote-desluggify title)
|
||
(file-name-base file)))
|
||
|
||
(defun denote--file-with-temp-buffer-subr (file)
|
||
"Return path to FILE or its buffer together with the appropriate function.
|
||
Subroutine of `denote--file-with-temp-buffer'."
|
||
(when file
|
||
(let* ((buffer (get-file-buffer file))
|
||
(file-exists (file-exists-p file))
|
||
(buffer-modified (buffer-modified-p buffer)))
|
||
(cond
|
||
((or (and file-exists
|
||
buffer
|
||
(not buffer-modified)
|
||
(not (eq buffer-modified 'autosaved)))
|
||
(and file-exists (not buffer)))
|
||
(cons #'insert-file-contents file))
|
||
(buffer
|
||
(cons #'insert-buffer buffer))
|
||
(t
|
||
(error "Cannot find anything about file `%s'" file))))))
|
||
|
||
(defmacro denote--file-with-temp-buffer (file &rest body)
|
||
"If FILE exists, insert its contents in a temp buffer and call BODY."
|
||
(declare (indent 1))
|
||
`(when-let ((file-and-function (denote--file-with-temp-buffer-subr ,file)))
|
||
(with-temp-buffer
|
||
(funcall (car file-and-function) (cdr file-and-function))
|
||
(goto-char (point-min))
|
||
,@body)))
|
||
|
||
(defun denote-retrieve-title-value (file file-type)
|
||
"Return title value from FILE front matter per FILE-TYPE."
|
||
(denote--file-with-temp-buffer file
|
||
(when (re-search-forward (denote--title-key-regexp file-type) nil t 1)
|
||
(funcall (denote--title-value-reverse-function file-type)
|
||
(buffer-substring-no-properties (point) (line-end-position))))))
|
||
|
||
(defun denote-retrieve-title-line (file file-type)
|
||
"Return title line from FILE front matter per FILE-TYPE."
|
||
(denote--file-with-temp-buffer file
|
||
(when (re-search-forward (denote--title-key-regexp file-type) nil t 1)
|
||
(buffer-substring-no-properties (line-beginning-position) (line-end-position)))))
|
||
|
||
(defun denote-retrieve-keywords-value (file file-type)
|
||
"Return keywords value from FILE front matter per FILE-TYPE.
|
||
The return value is a list of strings. To get a combined string
|
||
the way it would appear in a Denote file name, use
|
||
`denote-retrieve-keywords-value-as-string'."
|
||
(denote--file-with-temp-buffer file
|
||
(when (re-search-forward (denote--keywords-key-regexp file-type) nil t 1)
|
||
(funcall (denote--keywords-value-reverse-function file-type)
|
||
(buffer-substring-no-properties (point) (line-end-position))))))
|
||
|
||
(defun denote-retrieve-keywords-value-as-string (file file-type)
|
||
"Return keywords value from FILE front matter per FILE-TYPE.
|
||
The return value is a string, with the underscrore as a separator
|
||
between individual keywords. To get a list of strings instead,
|
||
use `denote-retrieve-keywords-value' (the current function uses
|
||
that internally)."
|
||
(denote-keywords-combine (denote-retrieve-keywords-value file file-type)))
|
||
|
||
(defun denote-retrieve-keywords-line (file file-type)
|
||
"Return keywords line from FILE front matter per FILE-TYPE."
|
||
(denote--file-with-temp-buffer file
|
||
(when (re-search-forward (denote--keywords-key-regexp file-type) nil t 1)
|
||
(buffer-substring-no-properties (line-beginning-position) (line-end-position)))))
|
||
|
||
(defun denote--retrieve-title-or-filename (file type)
|
||
"Return appropriate title for FILE given its TYPE."
|
||
(if-let (((denote-file-is-note-p file))
|
||
(title (denote-retrieve-title-value file type))
|
||
((not (string-blank-p title))))
|
||
title
|
||
(denote-retrieve-filename-title file)))
|
||
|
||
(defun denote--retrieve-location-in-xrefs (identifier)
|
||
"Return list of xrefs for IDENTIFIER with their respective location.
|
||
Limit the search to text files, per `denote-directory-text-only-files'."
|
||
(mapcar #'xref-match-item-location
|
||
(xref-matches-in-files identifier
|
||
(denote-directory-text-only-files))))
|
||
|
||
(defun denote--retrieve-group-in-xrefs (identifier)
|
||
"Access location of xrefs for IDENTIFIER and group them per file.
|
||
See `denote--retrieve-locations-in-xrefs'."
|
||
(mapcar #'xref-location-group
|
||
(denote--retrieve-location-in-xrefs identifier)))
|
||
|
||
(defun denote--retrieve-files-in-xrefs (identifier)
|
||
"Return sorted, deduplicated file names with IDENTIFIER in their contents."
|
||
(sort
|
||
(delete-dups
|
||
(denote--retrieve-group-in-xrefs identifier))
|
||
#'string-collate-lessp))
|
||
|
||
;;;; New note
|
||
|
||
;;;;; Common helpers for new notes
|
||
|
||
(defun denote-format-file-name (path id keywords title-slug extension &optional signature)
|
||
"Format file name.
|
||
PATH, ID, KEYWORDS, TITLE-SLUG, EXTENSION and optional SIGNATURE
|
||
are expected to be supplied by `denote' or equivalent command."
|
||
(let ((kws (denote-keywords-combine keywords))
|
||
(file-name (concat path id)))
|
||
(when (and signature (not (string-empty-p signature)))
|
||
(setq file-name (concat file-name "==" signature)))
|
||
(when (and title-slug (not (string-empty-p title-slug)))
|
||
(setq file-name (concat file-name "--" title-slug)))
|
||
(when (and keywords (not (string-blank-p kws)))
|
||
(setq file-name (concat file-name "__" kws)))
|
||
(concat file-name extension)))
|
||
|
||
(defun denote--format-front-matter-title (title file-type)
|
||
"Format TITLE according to FILE-TYPE for the file's front matter."
|
||
(funcall (denote--title-value-function file-type) title))
|
||
|
||
(defun denote--format-front-matter-keywords (keywords file-type)
|
||
"Format KEYWORDS according to FILE-TYPE for the file's front matter.
|
||
Apply `denote-letter-case' to KEYWORDS."
|
||
(let ((kw (denote-sluggify-keywords keywords)))
|
||
(funcall (denote--keywords-value-function file-type) kw)))
|
||
|
||
(defun denote--format-front-matter (title date keywords id filetype)
|
||
"Front matter for new notes.
|
||
|
||
TITLE, DATE, and ID are all strings or functions that return a
|
||
string. KEYWORDS is a list of strings. FILETYPE is one of the
|
||
values of `denote-file-type'."
|
||
(let* ((fm (denote--front-matter filetype))
|
||
(title (denote--format-front-matter-title title filetype))
|
||
(kws (denote--format-front-matter-keywords keywords filetype)))
|
||
(if fm (format fm title date kws id) "")))
|
||
|
||
(defun denote--path (title keywords dir id file-type &optional signature)
|
||
"Return path to new file.
|
||
Use ID, TITLE, KEYWORDS, FILE-TYPE and optional SIGNATURE to
|
||
construct path to DIR."
|
||
(denote-format-file-name
|
||
dir id
|
||
(denote-sluggify-keywords keywords)
|
||
(denote-sluggify title 'title)
|
||
(denote--file-extension file-type)
|
||
(when signature
|
||
(denote-sluggify-signature signature))))
|
||
|
||
;; 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."
|
||
(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."
|
||
(format-time-string "[%F %a %R]" date))
|
||
|
||
(defun denote-date-iso-8601 (date)
|
||
"Format DATE according to ISO 8601 standard."
|
||
(format-time-string "%F" date))
|
||
|
||
(defun denote--date (date file-type)
|
||
"Expand DATE in an appropriate format for FILE-TYPE."
|
||
(let ((format denote-date-format))
|
||
(cond
|
||
((stringp format)
|
||
(format-time-string format date))
|
||
((when-let ((fn (denote--date-format-function file-type)))
|
||
(funcall fn date)))
|
||
(t
|
||
(denote-date-org-timestamp date)))))
|
||
|
||
(defun denote--prepare-note (title keywords date id directory file-type template &optional signature)
|
||
"Prepare a new note file.
|
||
|
||
Arguments TITLE, KEYWORDS, DATE, ID, DIRECTORY, FILE-TYPE,
|
||
TEMPLATE, and optional SIGNATURE should be valid for note
|
||
creation."
|
||
(let* ((path (denote--path title keywords directory id file-type signature))
|
||
(buffer (find-file path))
|
||
(header (denote--format-front-matter
|
||
title (denote--date date file-type) keywords
|
||
(format-time-string denote-id-format date)
|
||
file-type)))
|
||
(with-current-buffer buffer
|
||
(insert header)
|
||
(insert template))))
|
||
|
||
(defun denote--dir-in-denote-directory-p (directory)
|
||
"Return non-nil if DIRECTORY is in variable `denote-directory'."
|
||
(and directory
|
||
(string-prefix-p (denote-directory)
|
||
(expand-file-name directory))))
|
||
|
||
(defun denote--valid-file-type (filetype)
|
||
"Return a valid filetype given the argument FILETYPE.
|
||
If none is found, the first element of `denote-file-types' is
|
||
returned."
|
||
(unless (or (symbolp filetype) (stringp filetype))
|
||
(user-error "`%s' is not a symbol or string" filetype))
|
||
(when (stringp filetype)
|
||
(setq filetype (intern filetype)))
|
||
(if (memq filetype (mapcar 'car denote-file-types))
|
||
filetype
|
||
(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))
|
||
|
||
(defun denote--valid-date (date)
|
||
"Return DATE if parsed by `date-to-time', else signal error."
|
||
(let ((datetime (denote--date-add-current-time date)))
|
||
(date-to-time datetime)))
|
||
|
||
(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-filename-is-note-p file)))
|
||
file))
|
||
(buffer-list))))
|
||
|
||
(defun denote--id-exists-p (identifier)
|
||
"Return non-nil if IDENTIFIER already exists."
|
||
(seq-some
|
||
(lambda (file)
|
||
(string-prefix-p identifier (file-name-nondirectory file)))
|
||
(append (denote-directory-files) (denote--buffer-file-names))))
|
||
|
||
(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)))
|
||
(names (append file-names (denote--buffer-file-names))))
|
||
(dolist (name names)
|
||
(let ((id (when (string-match (concat "\\`" denote-id-regexp) name)
|
||
(match-string-no-properties 0 name))))
|
||
(puthash id t ids)))
|
||
ids))
|
||
|
||
(defun denote--find-first-unused-id (id used-ids)
|
||
"Return the first unused id starting at ID from USED-IDS.
|
||
USED-IDS is a hash-table of all used IDs. If ID is already used,
|
||
increment it 1 second at a time until an available id is found."
|
||
(let ((time (date-to-time id)))
|
||
(while (gethash
|
||
(format-time-string denote-id-format time)
|
||
used-ids)
|
||
(setq time (time-add time 1)))
|
||
(format-time-string denote-id-format time)))
|
||
|
||
(make-obsolete 'denote-barf-duplicate-id nil "2.1.0")
|
||
|
||
(defvar denote--command-prompt-history nil
|
||
"Minibuffer history for `denote-command-prompt'.")
|
||
|
||
(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-commands-for-new-notes nil :require-match
|
||
nil 'denote--command-prompt-history default))))
|
||
|
||
;;;;; The `denote' command and its prompts
|
||
|
||
;;;###autoload
|
||
(defun denote (&optional title keywords file-type subdirectory date template signature)
|
||
"Create a new note with the appropriate metadata and file name.
|
||
|
||
Run the `denote-after-new-note-hook' after creating the new note.
|
||
|
||
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 `denote-file-type'.
|
||
|
||
- SUBDIRECTORY 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 SUBDIRECTORY does not resolve to a valid path, 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'.
|
||
|
||
- TEMPLATE is a symbol which represents the key of a cons cell in
|
||
the user option `denote-templates'. The value of that key is
|
||
inserted to the newly created buffer after the front matter.
|
||
|
||
- SIGNATURE is a string or a function returning a string."
|
||
(interactive
|
||
(let ((args (make-vector 7 nil)))
|
||
(dolist (prompt denote-prompts)
|
||
(pcase prompt
|
||
('title (aset args 0 (denote-title-prompt
|
||
(when (use-region-p)
|
||
(buffer-substring-no-properties
|
||
(region-beginning)
|
||
(region-end))))))
|
||
('keywords (aset args 1 (denote-keywords-prompt)))
|
||
('file-type (aset args 2 (denote-file-type-prompt)))
|
||
('subdirectory (aset args 3 (denote-subdirectory-prompt)))
|
||
('date (aset args 4 (denote-date-prompt)))
|
||
('template (aset args 5 (denote-template-prompt)))
|
||
('signature (aset args 6 (denote-signature-prompt)))))
|
||
(append args nil)))
|
||
(let* ((title (or title ""))
|
||
(file-type (denote--valid-file-type (or file-type denote-file-type)))
|
||
(kws (if (called-interactively-p 'interactive)
|
||
keywords
|
||
(denote-keywords-sort keywords)))
|
||
(date (if (or (null date) (string-empty-p date))
|
||
(current-time)
|
||
(denote--valid-date date)))
|
||
(id (denote--find-first-unused-id
|
||
(format-time-string denote-id-format date)
|
||
(denote--get-all-used-ids)))
|
||
(directory (if (denote--dir-in-denote-directory-p subdirectory)
|
||
(file-name-as-directory subdirectory)
|
||
(denote-directory)))
|
||
(template (if (stringp template)
|
||
template
|
||
(or (alist-get template denote-templates) "")))
|
||
(signature (or signature "")))
|
||
(denote--prepare-note title kws date id directory file-type template signature)
|
||
(denote--keywords-add-to-history keywords)
|
||
(run-hooks 'denote-after-new-note-hook)))
|
||
|
||
(defvar denote--title-history nil
|
||
"Minibuffer history of `denote-title-prompt'.")
|
||
|
||
(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' and use `unwind-protect' to set its value
|
||
back to nil.")
|
||
|
||
(defun denote-title-prompt (&optional default-title prompt-text)
|
||
"Read file title for `denote'.
|
||
With optional DEFAULT-TITLE use it as the default value. With
|
||
optional PROMPT-TEXT use it in the minibuffer instead of the
|
||
generic prompt.
|
||
|
||
Previous inputs at this prompt are available for minibuffer
|
||
completion. Consider `savehist-mode' to persist minibuffer
|
||
histories between sessions."
|
||
;; 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)))
|
||
(let ((def (or default-title denote-title-prompt-current-default)))
|
||
(completing-read
|
||
(format-prompt (or prompt-text "File title") def)
|
||
denote--title-history
|
||
nil nil nil 'denote--title-history def))))
|
||
|
||
(defvar denote--file-type-history nil
|
||
"Minibuffer history of `denote-file-type-prompt'.")
|
||
|
||
(defun denote-file-type-prompt ()
|
||
"Prompt for `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'.")
|
||
|
||
(declare-function org-read-date "org" (&optional with-time to-time from-string prompt default-time default-input inactive))
|
||
|
||
(defun denote-date-prompt ()
|
||
"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."
|
||
(if (and denote-date-prompt-use-org-read-date
|
||
(require 'org nil :no-error))
|
||
(let* ((time (org-read-date nil t))
|
||
(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
|
||
"DATE and TIME for note (e.g. 2022-06-16 14:30): "
|
||
nil 'denote--date-history)))
|
||
|
||
(defun denote-prompt-for-date-return-id ()
|
||
"Use `denote-date-prompt' and return it as `denote-id-format'."
|
||
(format-time-string
|
||
denote-id-format
|
||
(denote--valid-date (denote-date-prompt))))
|
||
|
||
(defvar denote--subdir-history nil
|
||
"Minibuffer history of `denote-subdirectory-prompt'.")
|
||
|
||
;; Making it a completion table is useful for packages that read the
|
||
;; metadata, such as `marginalia' and `embark'.
|
||
(defun denote--subdirs-completion-table (dirs)
|
||
"Match DIRS as a completion table."
|
||
(let* ((def (car denote--subdir-history))
|
||
(table (denote--completion-table 'file dirs))
|
||
(prompt (if def
|
||
(format "Select subdirectory [%s]: " def)
|
||
"Select subdirectory: ")))
|
||
(completing-read prompt table nil t nil 'denote--subdir-history def)))
|
||
|
||
(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* ((root (directory-file-name (denote-directory)))
|
||
(subdirs (denote-directory-subdirectories))
|
||
(dirs (push root subdirs)))
|
||
(denote--subdirs-completion-table dirs)))
|
||
|
||
(defvar denote--template-history nil
|
||
"Minibuffer history of `denote-template-prompt'.")
|
||
|
||
(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--signature-history nil
|
||
"Minibuffer history of `denote-signature-prompt'.")
|
||
|
||
(defun denote-signature-prompt (&optional default-signature prompt-text)
|
||
"Prompt for signature string.
|
||
With optional DEFAULT-SIGNATURE use it as the default minibuffer
|
||
value. With optional PROMPT-TEXT use it in the minibuffer
|
||
instead of the default prompt.
|
||
|
||
Previous inputs at this prompt are available for minibuffer
|
||
completion. Consider `savehist-mode' to persist minibuffer
|
||
histories between sessions."
|
||
(denote-sluggify-signature
|
||
;; 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
|
||
(format-prompt (or prompt-text "Provide signature") nil)
|
||
denote--signature-history
|
||
nil nil nil 'denote--signature-history default-signature))))
|
||
|
||
;;;;; Convenience commands as `denote' variants
|
||
|
||
(defalias 'denote-create-note 'denote
|
||
"Alias for `denote' command.")
|
||
|
||
;;;###autoload
|
||
(defun denote-type ()
|
||
"Create note while prompting for a file type.
|
||
|
||
This is the equivalent to calling `denote' when `denote-prompts'
|
||
is set to \\='(file-type title keywords)."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts '(file-type title keywords)))
|
||
(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 to calling `denote' when `denote-prompts'
|
||
is set to \\='(date title keywords)."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts '(date title keywords)))
|
||
(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 equivalent to calling `denote' when `denote-prompts' is
|
||
set to \\='(subdirectory title keywords)."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts '(subdirectory title keywords)))
|
||
(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 equivalent to calling `denote' when `denote-prompts' is
|
||
set to \\='(template title keywords)."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts '(template title keywords)))
|
||
(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 to calling `denote' when `denote-prompts'
|
||
is set to \\='(signature title keywords)."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((denote-prompts '(signature title keywords)))
|
||
(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.
|
||
Prompt for title and keywords. With no active region, call
|
||
`denote' ordinarily (refer to its documentation for the
|
||
technicalities)."
|
||
(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
|
||
(denote (denote-title-prompt) (denote-keywords-prompt))
|
||
(push-mark (point))
|
||
(insert text)
|
||
(run-hook-with-args 'denote-region-after-new-note-functions (mark) (point)))
|
||
(call-interactively 'denote)))
|
||
|
||
;;;;; Other convenience commands
|
||
|
||
(defun denote--extract-title-from-file-history ()
|
||
"Extract last file title input from `file-name-history'."
|
||
(when-let ((file (car denote--file-history))
|
||
(title (expand-file-name file)))
|
||
(string-match (denote-directory) title)
|
||
(substring title (match-end 0))))
|
||
|
||
(defun denote--append-extracted-string-to-history (history)
|
||
"Append `denote--extract-title-from-file-history' to HISTORY."
|
||
(append
|
||
(list (denote--extract-title-from-file-history))
|
||
history))
|
||
|
||
(defun denote--command-with-title-history (command)
|
||
"Call COMMAND with modified title history.
|
||
|
||
Set the `denote-title-prompt-current-default' to the value of the
|
||
last user input of a file title search (per `denote-file-prompt').
|
||
|
||
This is what makes commands such as `denote-open-or-create' or
|
||
`denote-link-or-create' get what the user initially typed as the
|
||
default value for the title of the new note to be created."
|
||
(let ((denote--title-history
|
||
(denote--append-extracted-string-to-history denote--title-history)))
|
||
(unwind-protect
|
||
(progn
|
||
(setq denote-title-prompt-current-default (car denote--title-history))
|
||
(call-interactively command))
|
||
(setq denote-title-prompt-current-default nil))))
|
||
|
||
;;;###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.
|
||
|
||
If TARGET file does not exist, add the user input that was used
|
||
to search for it to the minibuffer history of the
|
||
`denote-file-prompt'. The user can then retrieve and possibly
|
||
further edit their last input, using it as the newly created
|
||
note's actual title. At the `denote-file-prompt' type
|
||
\\<minibuffer-local-map>\\[previous-history-element]."
|
||
(interactive (list (denote-file-prompt)))
|
||
(if (and target (file-exists-p target))
|
||
(find-file target)
|
||
(denote--command-with-title-history #'denote)))
|
||
|
||
;;;###autoload
|
||
(defun denote-open-or-create-with-command ()
|
||
"Visit TARGET file in variable `denote-directory'.
|
||
If file does not exist, invoke `denote' to create a file.
|
||
|
||
If TARGET file does not exist, add the user input that was used
|
||
to search for it to the minibuffer history of the
|
||
`denote-file-prompt'. The user can then retrieve and possibly
|
||
further edit their last input, using it as the newly created
|
||
note's actual title. At the `denote-file-prompt' type
|
||
\\<minibuffer-local-map>\\[previous-history-element]."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((target (denote-file-prompt)))
|
||
(if (and target (file-exists-p target))
|
||
(find-file target)
|
||
(denote--command-with-title-history (denote-command-prompt)))))
|
||
|
||
;;;###autoload
|
||
(defun denote-keywords-add (keywords)
|
||
"Prompt for KEYWORDS to add to the current note's front matter.
|
||
When called from Lisp, KEYWORDS is a list of strings.
|
||
|
||
Rename the file without further prompt so that its name reflects
|
||
the new front matter, per `denote-rename-file-using-front-matter'."
|
||
(interactive (list (denote-keywords-prompt)))
|
||
;; A combination of if-let and let, as we need to take into account
|
||
;; the scenario in which there are no keywords yet.
|
||
(if-let ((file (buffer-file-name))
|
||
((denote-file-is-note-p file))
|
||
(file-type (denote-filetype-heuristics file)))
|
||
(let* ((cur-keywords (denote-retrieve-keywords-value file file-type))
|
||
(new-keywords (if (and (stringp cur-keywords)
|
||
(string-blank-p cur-keywords))
|
||
keywords
|
||
(denote-keywords-sort
|
||
(seq-uniq (append keywords cur-keywords))))))
|
||
(denote-rewrite-keywords file new-keywords file-type)
|
||
(denote-rename-file-using-front-matter file t))
|
||
(user-error "Buffer not visiting a Denote file")))
|
||
|
||
(defun denote--keywords-delete-prompt (keywords)
|
||
"Prompt for one or more KEYWORDS.
|
||
In the case of multiple entries, those are separated by the
|
||
`crm-sepator', which typically is a comma. In such a case, the
|
||
output is sorted with `string-collate-lessp'."
|
||
(let ((choice (denote--keywords-crm keywords "Keyword to remove: ")))
|
||
(if denote-sort-keywords
|
||
(sort choice #'string-collate-lessp)
|
||
choice)))
|
||
|
||
;;;###autoload
|
||
(defun denote-keywords-remove ()
|
||
"Prompt for keywords in current note and remove them.
|
||
Keywords are retrieved from the file's front matter.
|
||
|
||
Rename the file without further prompt so that its name reflects
|
||
the new front matter, per `denote-rename-file-using-front-matter'."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(if-let ((file (buffer-file-name))
|
||
((denote-file-is-note-p file))
|
||
(file-type (denote-filetype-heuristics file)))
|
||
(when-let ((cur-keywords (denote-retrieve-keywords-value file file-type))
|
||
((or (listp cur-keywords) (not (string-blank-p cur-keywords))))
|
||
(del-keyword (denote--keywords-delete-prompt cur-keywords)))
|
||
(denote-rewrite-keywords
|
||
file
|
||
(seq-difference cur-keywords del-keyword)
|
||
file-type)
|
||
(denote-rename-file-using-front-matter file t))
|
||
(user-error "Buffer not visiting a Denote file")))
|
||
|
||
;;;; 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-capture-p ()
|
||
"Return Org `denote-file-type' if this is an `org-capture' buffer."
|
||
(and (bound-and-true-p org-capture-mode)
|
||
(derived-mode-p 'org-mode)
|
||
(string-match-p "\\`CAPTURE.*\\.org" (buffer-name))))
|
||
|
||
(defun denote-filetype-heuristics (file)
|
||
"Return likely file type of FILE.
|
||
Use the file extension to detect the file type of the file.
|
||
|
||
If more than one file type correspond to this file extension, use
|
||
the first file type for which the key-title-kegexp matches in the
|
||
file or, if none matches, use the first type with this file
|
||
extension in `denote-file-type'.
|
||
|
||
If no file types in `denote-file-types' has the file extension,
|
||
the file type is assumed to be the first of `denote-file-types'."
|
||
(if (denote--file-type-org-capture-p)
|
||
'org
|
||
(let* ((file-type)
|
||
(extension (denote-get-file-extension-sans-encryption file))
|
||
(types (denote--file-types-with-extension extension)))
|
||
(cond ((not types)
|
||
(setq file-type (caar denote-file-types)))
|
||
((= (length types) 1)
|
||
(setq file-type (caar types)))
|
||
(t
|
||
(if-let ((found-type
|
||
(seq-find
|
||
(lambda (type)
|
||
(denote--regexp-in-file-p (plist-get (cdr type) :title-key-regexp) file))
|
||
types)))
|
||
(setq file-type (car found-type))
|
||
(setq file-type (caar types)))))
|
||
file-type)))
|
||
|
||
(defun denote--file-attributes-time (file)
|
||
"Return `file-attribute-modification-time' of FILE as identifier."
|
||
(format-time-string
|
||
denote-id-format
|
||
(file-attribute-modification-time (file-attributes file))))
|
||
|
||
(defun denote-update-dired-buffers ()
|
||
"Update Dired buffers of variable `denote-directory'."
|
||
(mapc
|
||
(lambda (buf)
|
||
(with-current-buffer buf
|
||
(when (and (eq major-mode 'dired-mode)
|
||
(denote--dir-in-denote-directory-p default-directory))
|
||
(revert-buffer))))
|
||
(buffer-list)))
|
||
|
||
(defun denote--rename-buffer (old-name new-name)
|
||
"Rename OLD-NAME buffer to NEW-NAME, when appropriate."
|
||
(when-let ((buffer (find-buffer-visiting old-name)))
|
||
(with-current-buffer buffer
|
||
(set-visited-file-name new-name nil t))))
|
||
|
||
(defun denote-rename-file-and-buffer (old-name new-name)
|
||
"Rename file named OLD-NAME to NEW-NAME, updating buffer name."
|
||
(unless (string= (expand-file-name old-name) (expand-file-name new-name))
|
||
(cond
|
||
((derived-mode-p 'dired-mode)
|
||
(dired-rename-file old-name new-name nil))
|
||
;; FIXME 2023-11-03: 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).
|
||
|
||
;; ((vc-backend old-name)
|
||
;; (vc-rename-file old-name new-name))
|
||
(t
|
||
(rename-file old-name new-name nil)))
|
||
(denote--rename-buffer old-name new-name)))
|
||
|
||
(defun denote--add-front-matter (file title keywords id file-type)
|
||
"Prepend front matter to FILE if `denote-file-is-note-p'.
|
||
The TITLE, KEYWORDS ID, and FILE-TYPE are passed from the
|
||
renaming command and are used to construct a new front matter
|
||
block if appropriate."
|
||
(when-let ((date (denote--date (date-to-time id) file-type))
|
||
(new-front-matter (denote--format-front-matter title date keywords id file-type)))
|
||
(with-current-buffer (find-file-noselect file)
|
||
(goto-char (point-min))
|
||
(insert new-front-matter))))
|
||
|
||
(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--edit-front-matter-p (file file-type)
|
||
"Test if FILE should be subject to front matter rewrite.
|
||
Use FILE-TYPE to look for the front matter lines. This is
|
||
relevant for operations that insert or rewrite the front matter
|
||
in a Denote note.
|
||
|
||
For the purposes of this test, FILE is a Denote note when it
|
||
contains a title line, a keywords line or both."
|
||
(and (denote--regexp-in-file-p (denote--title-key-regexp file-type) file)
|
||
(denote--regexp-in-file-p (denote--keywords-key-regexp file-type) file)))
|
||
|
||
(defun denote-rewrite-keywords (file keywords file-type)
|
||
"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.
|
||
|
||
This is for use in `denote-keywords-add',`denote-keywords-remove',
|
||
`denote-dired-rename-marked-files', or related."
|
||
(with-current-buffer (find-file-noselect file)
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
(when (re-search-forward (denote--keywords-key-regexp file-type) nil t 1)
|
||
(goto-char (line-beginning-position))
|
||
(insert (denote--get-keywords-line-from-front-matter keywords file-type))
|
||
(delete-region (point) (line-end-position)))))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--rewrite-keywords
|
||
'denote-rewrite-keywords
|
||
"2.0.0")
|
||
|
||
(defun denote-rewrite-front-matter (file title keywords file-type &optional no-confirm)
|
||
"Rewrite front matter of note after `denote-rename-file'.
|
||
The FILE, TITLE, KEYWORDS, and FILE-TYPE are given by the
|
||
renaming command and are used to construct new front matter
|
||
values if appropriate.
|
||
|
||
With optional NO-CONFIRM, do not prompt to confirm the rewriting
|
||
of the front matter. Otherwise produce a `y-or-n-p' prompt to
|
||
that effect."
|
||
(when-let ((old-title-line (denote-retrieve-title-line file file-type))
|
||
(old-keywords-line (denote-retrieve-keywords-line file file-type))
|
||
(new-title-line (denote--get-title-line-from-front-matter title file-type))
|
||
(new-keywords-line (denote--get-keywords-line-from-front-matter keywords file-type)))
|
||
(with-current-buffer (find-file-noselect file)
|
||
(when (or no-confirm
|
||
(y-or-n-p (format
|
||
"Replace front matter?\n-%s\n+%s\n\n-%s\n+%s?"
|
||
(propertize old-title-line 'face 'error)
|
||
(propertize new-title-line 'face 'success)
|
||
(propertize old-keywords-line 'face 'error)
|
||
(propertize new-keywords-line 'face 'success))))
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
(re-search-forward (denote--title-key-regexp file-type) nil t 1)
|
||
(goto-char (line-beginning-position))
|
||
(insert new-title-line)
|
||
(delete-region (point) (line-end-position))
|
||
(goto-char (point-min))
|
||
(re-search-forward (denote--keywords-key-regexp file-type) nil t 1)
|
||
(goto-char (line-beginning-position))
|
||
(insert new-keywords-line)
|
||
(delete-region (point) (line-end-position))))))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote--rewrite-front-matter
|
||
'denote-rewrite-front-matter
|
||
"2.0.0")
|
||
|
||
;;;;; The renaming commands and their prompts
|
||
|
||
(defun denote--rename-dired-file-or-prompt ()
|
||
"Return Dired file at point, else prompt for one.
|
||
Throw error is FILE is not regular, else return FILE."
|
||
(or (dired-get-filename nil t)
|
||
(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."
|
||
(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 'error)
|
||
(propertize (file-name-nondirectory new-name) 'face 'success)))))
|
||
|
||
;; 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.")
|
||
|
||
;;;###autoload
|
||
(defun denote-rename-file (file title keywords signature &optional ask-date)
|
||
"Rename file and update existing front matter if appropriate.
|
||
|
||
If in Dired, consider FILE to be the one at point, else prompt
|
||
with minibuffer completion for one. When called from Lisp, FILE
|
||
is a filesystem path represented as a string.
|
||
|
||
If FILE has a Denote-compliant identifier, retain it while
|
||
updating the TITLE and KEYWORDS fields of the file name.
|
||
|
||
Else create an identifier based on the following conditions:
|
||
|
||
1. If optional ASK-DATE is non-nil (such as with a prefix
|
||
argument), prompt for a date and use it to derive the
|
||
identifier.
|
||
|
||
2. If optional ASK-DATE is nil (this is the case without a prefix
|
||
argument), use the file attributes to determine the last
|
||
modified date and format it as an identifier.
|
||
|
||
3. As a fallback, derive an identifier from the current time.
|
||
|
||
4. If the resulting identifier is not unique among the files in
|
||
the variable `denote-directory', increment it such that it
|
||
becomes unique.
|
||
|
||
Use TITLE to construct the new name of FILE. In interactive use,
|
||
retrieve the default TITLE value from a line starting with a
|
||
title field in the file's contents, depending on the given file
|
||
type (e.g. #+title for Org). Else, use the file name as a
|
||
default value at the minibuffer prompt. When called from Lisp,
|
||
TITLE is a string.
|
||
|
||
Add SIGNATURE to the file, using an existing one as the default
|
||
value at the minibuffer prompt. When called from Lisp, SIGNATURE
|
||
is a string. If the SIGNATURE is empty or nil, it is not
|
||
included in the new file name.
|
||
|
||
As a final step after the FILE, TITLE, KEYWORDS, and SIGNATURE
|
||
are collected, ask for confirmation, showing the difference
|
||
between old and new file names. Do not ask for confirmation if
|
||
the user option `denote-rename-no-confirm' is set to a non-nil
|
||
value.
|
||
|
||
Read the file type extension (like .txt) from the underlying file
|
||
and preserve it through the renaming process. Files that have no
|
||
extension are left without one.
|
||
|
||
Renaming only occurs relative to the current directory. Files
|
||
are not moved between directories.
|
||
|
||
If the FILE has Denote-style front matter for the TITLE and
|
||
KEYWORDS, ask to rewrite their values in order to reflect the new
|
||
input (this step always requires confirmation and the underlying
|
||
buffer is not saved, so consider invoking `diff-buffer-with-file'
|
||
to double-check the effect). The rewrite of the FILE and
|
||
KEYWORDS in the front matter should not affect the rest of the
|
||
front matter.
|
||
|
||
If the file doesn't have front matter but is among the supported
|
||
file types (per `denote-file-type'), add front matter at the top
|
||
of it and leave the buffer unsaved for further inspection.
|
||
|
||
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'
|
||
|
||
This command is intended to (i) rename existing Denote notes
|
||
while updating their title and keywords in the front matter, (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. The latter is a convenience we provide,
|
||
since we already have all the requisite mechanisms in
|
||
place."
|
||
(interactive
|
||
(let* ((file (denote--rename-dired-file-or-prompt))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(file-in-prompt (propertize file 'face 'error)))
|
||
(list
|
||
file
|
||
(denote-title-prompt
|
||
(denote--retrieve-title-or-filename file file-type)
|
||
(format "Rename `%s' with title" file-in-prompt))
|
||
(denote-keywords-prompt
|
||
(format "Rename `%s' with keywords" file-in-prompt))
|
||
(denote-signature-prompt
|
||
(denote-retrieve-filename-signature file)
|
||
(format "Rename `%s' with signature (empty to ignore)" file-in-prompt))
|
||
current-prefix-arg)))
|
||
(let* ((dir (file-name-directory file))
|
||
(id (or (denote-retrieve-filename-identifier file :no-error)
|
||
(denote-create-unique-file-identifier file (denote--get-all-used-ids) ask-date)))
|
||
(extension (denote-get-file-extension file))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(title (or title (denote--retrieve-title-or-filename file file-type)))
|
||
(keywords (or keywords (denote-retrieve-keywords-value file file-type)))
|
||
(signature (or signature (denote-retrieve-filename-signature file)))
|
||
(new-name (denote-format-file-name dir id keywords (denote-sluggify title 'title) extension (denote-sluggify-signature signature)))
|
||
(max-mini-window-height denote-rename-max-mini-window-height))
|
||
(when (or denote-rename-no-confirm (denote-rename-file-prompt file new-name))
|
||
(denote-rename-file-and-buffer file new-name)
|
||
(denote-update-dired-buffers)
|
||
(when (denote-file-is-writable-and-supported-p new-name)
|
||
(if (denote--edit-front-matter-p new-name file-type)
|
||
(denote-rewrite-front-matter new-name title keywords file-type denote-rename-no-confirm)
|
||
(denote--add-front-matter new-name title keywords id file-type))))
|
||
new-name))
|
||
|
||
;;;###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."
|
||
(declare (interactive-only t))
|
||
(interactive nil dired-mode)
|
||
(if-let ((marks (dired-get-marked-files)))
|
||
(let ((used-ids (when (seq-some
|
||
(lambda (m)
|
||
(not (denote-retrieve-filename-identifier m :no-error)))
|
||
marks)
|
||
(denote--get-all-used-ids))))
|
||
(dolist (file marks)
|
||
(let* ((file-type (denote-filetype-heuristics file))
|
||
(file-in-prompt (propertize file 'face 'error))
|
||
(dir (file-name-directory file))
|
||
(id (or (denote-retrieve-filename-identifier file :no-error)
|
||
(denote-create-unique-file-identifier file used-ids)))
|
||
(title (denote-title-prompt
|
||
(denote--retrieve-title-or-filename file file-type)
|
||
(format "Rename `%s' with title" file-in-prompt)))
|
||
(keywords (denote-keywords-prompt
|
||
(format "Rename `%s' with keywords" file-in-prompt)))
|
||
(signature (denote-signature-prompt
|
||
(denote-retrieve-filename-signature file)
|
||
(format "Rename `%s' with signature (empty to ignore)" file-in-prompt)))
|
||
(extension (denote-get-file-extension file))
|
||
(new-name (denote-format-file-name dir id keywords (denote-sluggify title 'title) extension (denote-sluggify-signature signature))))
|
||
(denote-rename-file-and-buffer file new-name)
|
||
(when (denote-file-is-writable-and-supported-p new-name)
|
||
(if (denote--edit-front-matter-p new-name file-type)
|
||
(denote-rewrite-front-matter new-name title keywords file-type denote-rename-no-confirm)
|
||
(denote--add-front-matter new-name title keywords id file-type)))
|
||
(when used-ids
|
||
(puthash id t used-ids))))
|
||
(denote-update-dired-buffers))
|
||
(user-error "No marked files; aborting")))
|
||
|
||
(make-obsolete
|
||
'denote-dired-rename-marked-files
|
||
'denote-dired-rename-marked-files-with-keywords
|
||
"2.1.0")
|
||
|
||
;;;###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;
|
||
|
||
- `denote-letter-case' and sluggify the TITLE, according to our
|
||
conventions (check the user option `denote-file-name-letter-casing');
|
||
|
||
- 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;
|
||
|
||
- add or rewrite existing front matter to the underlying file, if
|
||
it is recognized as a Denote note (per `denote-file-type'),
|
||
such that it includes the new keywords.
|
||
|
||
[ Note that the affected buffers are not saved. Users can thus
|
||
check them to confirm that the new front matter does not cause
|
||
any problems (e.g. with the `diff-buffer-with-file' command).
|
||
Multiple buffers can be saved in one go with the command
|
||
`save-some-buffers' (read its doc string). ]"
|
||
(declare (interactive-only t))
|
||
(interactive nil dired-mode)
|
||
(if-let ((marks (dired-get-marked-files)))
|
||
(let ((keywords (denote-keywords-prompt "Rename marked files with these keywords (overwrite existing)"))
|
||
(used-ids (when (seq-some
|
||
(lambda (m) (not (denote-retrieve-filename-identifier m :no-error)))
|
||
marks)
|
||
(denote--get-all-used-ids))))
|
||
(dolist (file marks)
|
||
(let* ((dir (file-name-directory file))
|
||
(id (or (denote-retrieve-filename-identifier file :no-error)
|
||
(denote-create-unique-file-identifier file used-ids)))
|
||
(signature (denote-retrieve-filename-signature file))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(title (denote--retrieve-title-or-filename file file-type))
|
||
(extension (denote-get-file-extension file))
|
||
(new-name (denote-format-file-name dir id keywords (denote-sluggify title 'title) extension (denote-sluggify-signature signature))))
|
||
(denote-rename-file-and-buffer file new-name)
|
||
(when (denote-file-is-writable-and-supported-p new-name)
|
||
(if (denote--edit-front-matter-p new-name file-type)
|
||
(denote-rewrite-keywords new-name keywords file-type)
|
||
(denote--add-front-matter new-name title keywords id file-type)))
|
||
(when used-ids
|
||
(puthash id t used-ids))))
|
||
(denote-update-dired-buffers))
|
||
(user-error "No marked files; aborting")))
|
||
|
||
;;;###autoload
|
||
(defun denote-rename-file-using-front-matter (file &optional auto-confirm)
|
||
"Rename FILE using its front matter as input.
|
||
When called interactively, FILE is the return value of the
|
||
function `buffer-file-name' 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 `denote-file-type'.
|
||
|
||
Unless AUTO-CONFIRM is non-nil (such as with a prefix argument),
|
||
ask for confirmation, showing the difference between the old and
|
||
the new file names.
|
||
|
||
Never modify the identifier of the FILE, if any, even if it is
|
||
edited in the front matter. Denote considers the file name to be
|
||
the source of truth in this case to avoid potential breakage with
|
||
typos and the like.
|
||
|
||
If AUTO-CONFIRM is non-nil, then proceed with the renaming
|
||
operation without prompting for confirmation. This is what the
|
||
command `denote-dired-rename-marked-files-using-front-matter'
|
||
does internally."
|
||
(interactive (list (buffer-file-name) current-prefix-arg))
|
||
(unless (denote-file-is-writable-and-supported-p file)
|
||
(user-error "The file is not writable or does not have a supported file extension"))
|
||
(if-let ((file-type (denote-filetype-heuristics file))
|
||
(title (denote-retrieve-title-value file file-type))
|
||
(id (denote-retrieve-filename-identifier file :no-error)))
|
||
(let* ((sluggified-title (denote-sluggify title 'title))
|
||
(keywords (denote-retrieve-keywords-value file file-type))
|
||
(signature (denote-retrieve-filename-signature file))
|
||
(extension (denote-get-file-extension file))
|
||
(dir (file-name-directory file))
|
||
(new-name (denote-format-file-name dir id keywords sluggified-title extension signature)))
|
||
(when (or auto-confirm
|
||
(denote-rename-file-prompt file new-name))
|
||
(denote-rename-file-and-buffer file new-name)
|
||
(denote-update-dired-buffers)))
|
||
(user-error "No identifier or front matter for title")))
|
||
|
||
;;;###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 `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 (denote-file-is-writable-and-supported-p m)
|
||
(denote-retrieve-filename-identifier m :no-error)))
|
||
(dired-get-marked-files))))
|
||
(progn
|
||
(dolist (file marks)
|
||
(denote-rename-file-using-front-matter file :auto-confirm))
|
||
(denote-update-dired-buffers))
|
||
(user-error "No marked Denote files; aborting")))
|
||
|
||
;;;;; Creation of front matter
|
||
|
||
;;;###autoload
|
||
(defun denote-add-front-matter (file title keywords)
|
||
"Insert front matter at the top of FILE.
|
||
|
||
When called interactively, FILE is the return value of the
|
||
function `buffer-file-name'. FILE is checked to determine
|
||
whether it is a note for Denote's purposes.
|
||
|
||
TITLE is a string. Interactively, it is the user input at the
|
||
minibuffer prompt.
|
||
|
||
KEYWORDS is a list of strings. Interactively, it is the user
|
||
input at the minibuffer prompt. This one supports completion for
|
||
multiple entries, each separated by the `crm-separator' (normally
|
||
a comma).
|
||
|
||
The purpose of this command is to help the user generate new
|
||
front matter for an existing note (perhaps because the user
|
||
deleted the previous one and could not undo the change).
|
||
|
||
This command does not rename the file (e.g. to update the
|
||
keywords). To rename a file by reading its front matter as
|
||
input, use `denote-rename-file-using-front-matter'.
|
||
|
||
Note that this command is useful only for existing Denote notes.
|
||
If the user needs to convert a generic text file to a Denote
|
||
note, they can use one of the command which first rename the file
|
||
to make it comply with our file-naming scheme and then add the
|
||
relevant front matter."
|
||
(interactive
|
||
(list
|
||
(buffer-file-name)
|
||
(denote-title-prompt)
|
||
(denote-keywords-prompt)))
|
||
(when (and (denote-file-is-writable-and-supported-p file)
|
||
(denote-retrieve-filename-identifier file :no-error))
|
||
(denote--add-front-matter
|
||
file title keywords
|
||
(denote-retrieve-filename-identifier file)
|
||
(denote-filetype-heuristics file))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-change-file-type
|
||
'denote-change-file-type-and-front-matter
|
||
"2.1.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 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."
|
||
(interactive
|
||
(list
|
||
(denote--rename-dired-file-or-prompt)
|
||
(denote--valid-file-type (or (denote-file-type-prompt) denote-file-type))))
|
||
(let* ((dir (file-name-directory file))
|
||
(old-file-type (denote-filetype-heuristics file))
|
||
(id (or (denote-retrieve-filename-identifier file :no-error) ""))
|
||
(title (denote-retrieve-title-value file old-file-type))
|
||
(keywords (denote-retrieve-keywords-value file old-file-type))
|
||
(old-extension (denote-get-file-extension file))
|
||
(new-extension (denote--file-extension new-file-type))
|
||
(new-name (denote-format-file-name
|
||
dir id keywords (denote-sluggify title 'title) new-extension))
|
||
(max-mini-window-height 0.33)) ; allow minibuffer to be resized
|
||
(when (and (not (eq old-extension new-extension))
|
||
(denote-rename-file-prompt file new-name))
|
||
(denote-rename-file-and-buffer file new-name)
|
||
(denote-update-dired-buffers)
|
||
(when (denote-file-is-writable-and-supported-p new-name)
|
||
(denote--add-front-matter new-name title keywords id new-file-type)))))
|
||
|
||
;;;; 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-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-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"))
|
||
|
||
;; For character classes, evaluate: (info "(elisp) Char Classes")
|
||
(defvar denote-faces--file-name-regexp
|
||
(concat "\\(?1:[0-9]\\{8\\}\\)\\(?10:T\\)\\(?2:[0-9]\\{6\\}\\)"
|
||
"\\(?:\\(?3:==\\)\\(?4:[[:alnum:][:nonascii:]=]*?\\)\\)?"
|
||
"\\(?:\\(?5:--\\)\\(?6:[[:alnum:][:nonascii:]-]*?\\)\\)?"
|
||
"\\(?:\\(?7:__\\)\\(?8:[[:alnum:][:nonascii:]_-]*?\\)\\)?"
|
||
"\\(?9:\\..*\\)?$")
|
||
"Regexp of file names for fontification.")
|
||
|
||
(defconst denote-faces-file-name-keywords
|
||
`((,(concat "[\t\s]+" denote-faces--file-name-regexp)
|
||
(1 'denote-faces-date)
|
||
(10 'denote-faces-time-delimiter nil t)
|
||
(2 'denote-faces-time)
|
||
(3 'denote-faces-delimiter nil t)
|
||
(4 'denote-faces-signature nil t)
|
||
(5 'denote-faces-delimiter nil t)
|
||
(6 'denote-faces-title nil t)
|
||
(7 'denote-faces-delimiter nil t)
|
||
(8 'denote-faces-keywords nil t)
|
||
(9 'denote-faces-extension nil t )))
|
||
"Keywords for fontification of file names.")
|
||
|
||
(defconst denote-faces-file-name-keywords-for-backlinks
|
||
`((,(concat "^\\(?11:.*/\\)?" denote-faces--file-name-regexp)
|
||
(11 'denote-faces-subdirectory nil t)
|
||
(1 'denote-faces-date)
|
||
(10 'denote-faces-time-delimiter nil t)
|
||
(2 'denote-faces-time)
|
||
(3 'denote-faces-delimiter nil t)
|
||
(4 'denote-faces-signature nil t)
|
||
(5 'denote-faces-delimiter nil t)
|
||
(6 'denote-faces-title nil t)
|
||
(7 'denote-faces-delimiter nil t)
|
||
(8 'denote-faces-keywords nil t)
|
||
(9 'denote-faces-extension nil t )))
|
||
"Keywords for fontification of file names in the backlinks buffer.")
|
||
|
||
;;;; Fontification in Dired
|
||
|
||
(defgroup denote-dired ()
|
||
"Integration between Denote and Dired."
|
||
:group 'denote)
|
||
|
||
(defcustom denote-dired-directories (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'."
|
||
:type '(repeat directory)
|
||
:package-version '(denote . "0.1.0")
|
||
:link '(info-link "(denote) Fontification in Dired")
|
||
: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 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)))
|
||
|
||
(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'."
|
||
(when (member (file-truename default-directory) (denote-dired--modes-dirs-as-dirs))
|
||
(denote-dired-mode 1)))
|
||
|
||
;;;; The linking facility
|
||
|
||
(defgroup denote-link ()
|
||
"Link facility for Denote."
|
||
:group 'denote)
|
||
|
||
;;;;; User options
|
||
|
||
(defcustom denote-link-backlinks-display-buffer-action
|
||
'((display-buffer-reuse-window display-buffer-below-selected)
|
||
(window-height . fit-window-to-buffer))
|
||
"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-link-backlinks-display-buffer-action
|
||
(quote ((display-buffer-reuse-window
|
||
display-buffer-in-side-window)
|
||
(side . left)
|
||
(slot . 99)
|
||
(window-width . 0.3))))
|
||
|
||
See Info node `(elisp) Displaying Buffers' for more details
|
||
and/or the documentation string of `display-buffer'."
|
||
:type '(cons (choice (function :tag "Display Function")
|
||
(repeat :tag "Display Functions" function))
|
||
alist)
|
||
:package-version '(denote . "0.1.0")
|
||
:group 'denote-link)
|
||
|
||
;;;;; 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:" denote-id-regexp "\\)" "]" "\\[.*?]]")
|
||
"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 "\\[.*?]" "(denote:" "\\(?1:" denote-id-regexp "\\)" ")")
|
||
"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:" denote-id-regexp "\\)" "]]")
|
||
"Regexp to match an identifier-only link in its context.
|
||
The format of such links is `denote-id-only-link-format'." )
|
||
|
||
(defun denote-link--file-type-format (file-type id-only)
|
||
"Return link format based on FILE-TYPE.
|
||
With non-nil ID-ONLY, use the generic link format without a
|
||
title.
|
||
|
||
Fall back to `denote-org-link-format'."
|
||
;; Includes backup files. Maybe we can remove them?
|
||
(cond
|
||
(id-only denote-id-only-link-format)
|
||
((when-let ((link (denote--link-format file-type)))
|
||
link))
|
||
;; Plain text also uses [[denote:ID][TITLE]]
|
||
(t denote-org-link-format)))
|
||
|
||
(defun denote-format-link (file format description)
|
||
"Prepare link to FILE using FORMAT and DESCRIPTION text.
|
||
FILE is the path to a file name. FORMAT is the symbol of a
|
||
variable that specifies a string. See the `:link' property of
|
||
`denote-file-types'.
|
||
|
||
DESCRIPTION is the text of the link. If nil, DESCRIPTION is
|
||
retrieved from the FILE, unless the FORMAT is
|
||
`denote-id-only-link-format'."
|
||
(let* ((file-id (denote-retrieve-filename-identifier file))
|
||
(fm (if (symbolp format) (symbol-value format) format))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(file-title (unless (string= fm denote-id-only-link-format)
|
||
(or description (denote--retrieve-title-or-filename file file-type)))))
|
||
(format fm file-id file-title)))
|
||
|
||
(make-obsolete 'denote-link--format-link 'denote-format-link "2.1.0")
|
||
|
||
(defun denote--link-get-description (file file-type)
|
||
"Return description for `denote-link'.
|
||
If the region is active, make the description the text within the
|
||
region's boundaries. Else retrieve the title from FILE, given
|
||
FILE-TYPE.
|
||
|
||
Also see `denote--link-get-description-with-signature'."
|
||
(if-let (((region-active-p))
|
||
(beg (region-beginning))
|
||
(end (region-end))
|
||
(selected-text (string-trim (buffer-substring-no-properties beg end))))
|
||
(progn
|
||
(delete-region beg end)
|
||
selected-text)
|
||
(denote--retrieve-title-or-filename file file-type)))
|
||
|
||
;;;###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 selected FILE, as well as
|
||
the DESCRIPTION from the title of FILE. The title comes either
|
||
from the front matter or the file name. With an active region,
|
||
the DESCRIPTION is the text of the region, despite the
|
||
aforementioned. If active region is empty (i.e whitespace-only),
|
||
insert an ID-ONLY link.
|
||
|
||
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]].
|
||
|
||
When called from Lisp, FILE is a string representing a full file
|
||
system path. FILE-TYPE is a symbol as described in
|
||
`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))
|
||
(type (denote-filetype-heuristics (buffer-file-name))))
|
||
(list
|
||
file
|
||
type
|
||
(denote--link-get-description file type)
|
||
current-prefix-arg)))
|
||
(let* ((beg (point))
|
||
(identifier-only (or id-only (string-empty-p description))))
|
||
(when file
|
||
(insert
|
||
(denote-format-link
|
||
file
|
||
(denote-link--file-type-format file-type identifier-only)
|
||
description))
|
||
(unless (derived-mode-p 'org-mode)
|
||
(make-button beg (point) 'type 'denote-link-button)))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-insert-link
|
||
'denote-insert-link
|
||
"2.0.0")
|
||
|
||
(defalias 'denote-insert-link 'denote-link
|
||
"Alias for `denote-link' command.")
|
||
|
||
(defun denote--link-get-description-with-signature (file file-type)
|
||
"Return `denote-link-with-signature' description.
|
||
Retrieve the title and signature from FILE with FILE-TYPE. If
|
||
the region is active, use it to describe the link instead of the
|
||
file's title. Make the signature a prefix. If there is no title
|
||
or text in the active region, return the signature on its own.
|
||
|
||
Also see `denote--link-get-description'."
|
||
(let* ((signature (denote-retrieve-filename-signature file))
|
||
(text (denote--link-get-description file file-type))
|
||
(specifiers (if (and text
|
||
(not (string-empty-p text)))
|
||
"%s %s"
|
||
"%s")))
|
||
(format specifiers signature text)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-with-signature ()
|
||
"Insert link to file with signature.
|
||
Prompt for file using minibuffer completion, limiting the list of
|
||
candidates to files with a signature in their file name.
|
||
|
||
The description of the link includes the signature followed by
|
||
the file's title, if any. For this case, the signature is
|
||
assumed present.
|
||
|
||
For more advanced uses with Lisp, refer to the `denote-link'
|
||
function."
|
||
(declare (interactive-only t))
|
||
(interactive)
|
||
(let ((file (denote-file-prompt "="))
|
||
(type (denote-filetype-heuristics (buffer-file-name))))
|
||
(denote-link file type (denote--link-get-description-with-signature file type))))
|
||
|
||
(defun denote-link--collect-identifiers (regexp)
|
||
"Return collection of identifiers in buffer matching REGEXP."
|
||
(let (matches)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(while (or (re-search-forward regexp nil t)
|
||
(re-search-forward denote-id-only-link-in-context-regexp nil t))
|
||
(push (match-string-no-properties 1) matches)))
|
||
matches))
|
||
|
||
(defun denote-link--expand-identifiers (regexp)
|
||
"Expend identifiers matching REGEXP into file paths."
|
||
(let ((files (denote-directory-files))
|
||
(rx (if (symbolp regexp) (symbol-value regexp) regexp))
|
||
found-files)
|
||
(dolist (file files)
|
||
(dolist (i (denote-link--collect-identifiers rx))
|
||
(when (string-prefix-p i (file-name-nondirectory file))
|
||
(push file found-files))))
|
||
found-files))
|
||
|
||
(defvar denote-link--find-file-history nil
|
||
"History for `denote-find-link'.")
|
||
|
||
(defun denote-link--find-file-prompt (files)
|
||
"Prompt for linked file among FILES."
|
||
(let ((file-names (mapcar #'denote-get-file-name-relative-to-denote-directory
|
||
files)))
|
||
(completing-read
|
||
"Find linked file: "
|
||
(denote--completion-table 'file file-names)
|
||
nil t nil 'denote-link--find-file-history)))
|
||
|
||
(defun denote-link-return-links (&optional file)
|
||
"Return list of links in current or optional FILE.
|
||
Also see `denote-link-return-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))
|
||
(links (with-current-buffer (find-file-noselect current-file)
|
||
(denote-link--expand-identifiers regexp))))
|
||
links))
|
||
|
||
(defalias 'denote-link-return-forelinks 'denote-link-return-links
|
||
"Alias for `denote-link-return-links'.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-find-file
|
||
'denote-find-link
|
||
"2.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-find-link ()
|
||
"Use minibuffer completion to visit linked file."
|
||
(interactive)
|
||
(find-file
|
||
(denote-link--find-file-prompt
|
||
(or (denote-link-return-links)
|
||
(user-error "No links found")))))
|
||
|
||
(defun denote-link-return-backlinks (&optional file)
|
||
"Return list of backlinks in current or optional FILE.
|
||
Also see `denote-link-return-links'."
|
||
(when-let ((current-file (or file (buffer-file-name)))
|
||
(id (denote-retrieve-filename-identifier current-file))
|
||
(backlinks (delete current-file (denote--retrieve-files-in-xrefs id))))
|
||
backlinks))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-find-backlink
|
||
'denote-find-backlink
|
||
"2.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-find-backlink ()
|
||
"Use minibuffer completion to visit backlink to current file.
|
||
|
||
Like `denote-find-link', but select backlink to follow."
|
||
(interactive)
|
||
(find-file
|
||
(denote-get-path-by-id
|
||
(denote-extract-id-from-string
|
||
(denote-link--find-file-prompt
|
||
(or (denote-link-return-backlinks)
|
||
(user-error "No backlinks found")))))))
|
||
|
||
(defun denote--link-after-creating-subr (command description-fn &optional id-only)
|
||
"Subroutine for `denote-link-after-creating' and the like.
|
||
COMMAND is the symbol of a file-creating command to call, such as
|
||
`denote' or `denote-signature'.
|
||
|
||
DESCRIPTION-FN is the symbol of a function that returns the
|
||
description of a link, like `denote--link-get-description' or
|
||
`denote--link-get-description-with-signature'.
|
||
|
||
ID-ONLY has the same meaning as described in `denote-link'."
|
||
(let (path)
|
||
(save-window-excursion
|
||
(call-interactively command)
|
||
(save-buffer)
|
||
(setq path (buffer-file-name)))
|
||
(let ((type (denote-filetype-heuristics path)))
|
||
(denote-link path type (funcall description-fn path type) id-only))))
|
||
|
||
;;;###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."
|
||
(interactive "P")
|
||
(denote--link-after-creating-subr #'denote #'denote--link-get-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))
|
||
(denote--link-after-creating-subr
|
||
command
|
||
(if (eq command 'denote-signature)
|
||
#'denote--link-get-description-with-signature
|
||
#'denote--link-get-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.
|
||
|
||
If TARGET file does not exist, add the user input that was used
|
||
to search for it to the minibuffer history of the
|
||
`denote-file-prompt'. The user can then retrieve and possibly
|
||
further edit their last input, using it as the newly created
|
||
note's actual title. At the `denote-file-prompt' type
|
||
\\<minibuffer-local-map>\\[previous-history-element].
|
||
|
||
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 (list (denote-file-prompt) current-prefix-arg))
|
||
(if (and target (file-exists-p target))
|
||
(let ((type (denote-filetype-heuristics target)))
|
||
(denote-link
|
||
target
|
||
type
|
||
(denote--link-get-description target type)
|
||
id-only))
|
||
(denote--command-with-title-history #'denote-link-after-creating)))
|
||
|
||
(defalias 'denote-link-to-existing-or-new-note 'denote-link-or-create
|
||
"Alias for `denote-link-or-create' command.")
|
||
|
||
;;;;; Link buttons
|
||
|
||
;; Evaluate: (info "(elisp) Button Properties")
|
||
;;
|
||
;; Button can provide a help-echo function as well, but I think we might
|
||
;; not need it.
|
||
(define-button-type 'denote-link-button
|
||
'follow-link t
|
||
'face 'denote-faces-link
|
||
'action #'denote-link--find-file-at-button)
|
||
|
||
(autoload 'thing-at-point-looking-at "thingatpt")
|
||
|
||
(defun denote-link--link-at-point-string ()
|
||
"Return identifier at point."
|
||
(when (or (thing-at-point-looking-at denote-id-only-link-in-context-regexp)
|
||
(thing-at-point-looking-at denote-md-link-in-context-regexp)
|
||
(thing-at-point-looking-at denote-org-link-in-context-regexp)
|
||
;; Meant to handle the case where a link is broken by
|
||
;; `fill-paragraph' into two lines, in which case it
|
||
;; buttonizes only the "denote:ID" part. Example:
|
||
;;
|
||
;; [[denote:20220619T175212][This is a
|
||
;; test]]
|
||
;;
|
||
;; Maybe there is a better way?
|
||
(thing-at-point-looking-at "\\[\\(denote:.*\\)]"))
|
||
(match-string-no-properties 0)))
|
||
|
||
;; NOTE 2022-06-15: I add this as a variable for advanced users who may
|
||
;; prefer something else. If there is demand for it, we can make it a
|
||
;; defcustom, but I think it would be premature at this stage.
|
||
(defvar denote-link-button-action #'find-file-other-window
|
||
"Display buffer action for Denote buttons.")
|
||
|
||
(defun denote-link--find-file-at-button (button)
|
||
"Visit file referenced by BUTTON."
|
||
(let* ((id (denote-extract-id-from-string
|
||
(buffer-substring-no-properties
|
||
(button-start button)
|
||
(button-end button))))
|
||
(file (denote-get-path-by-id id)))
|
||
(funcall denote-link-button-action file)))
|
||
|
||
;;;###autoload
|
||
(defun denote-link-buttonize-buffer (&optional beg end)
|
||
"Make denote: links actionable buttons in the current buffer.
|
||
|
||
Buttonization applies to the plain text and Markdown file types,
|
||
per the user option `denote-file-types'. It will not do anything
|
||
in `org-mode' buffers, as buttons already work there. If you do
|
||
not use Markdown or plain text, then you do not need this.
|
||
|
||
Links work when they point to a file inside the variable
|
||
`denote-directory'.
|
||
|
||
To buttonize links automatically add this function to the
|
||
`find-file-hook'. Or call it interactively for on-demand
|
||
buttonization.
|
||
|
||
When called from Lisp, with optional BEG and END as buffer
|
||
positions, limit the process to the region in-between."
|
||
(interactive)
|
||
(when (and (not (derived-mode-p 'org-mode))
|
||
(denote-file-has-identifier-p (buffer-file-name)))
|
||
(save-excursion
|
||
(goto-char (or beg (point-min)))
|
||
(while (re-search-forward denote-id-regexp end t)
|
||
(when-let ((string (denote-link--link-at-point-string))
|
||
(beg (match-beginning 0))
|
||
(end (match-end 0)))
|
||
(make-button beg end 'type 'denote-link-button))))))
|
||
|
||
;;;;; Backlinks' buffer
|
||
|
||
(define-button-type 'denote-link-backlink-button
|
||
'follow-link t
|
||
'action #'denote-link--backlink-find-file
|
||
'face nil) ; we use this face though we style it later
|
||
|
||
(defun denote-link--backlink-find-file (button)
|
||
"Action for BUTTON to `find-file'."
|
||
(funcall denote-link-button-action (buffer-substring (button-start button) (button-end button))))
|
||
|
||
(defun denote-link--display-buffer (buf)
|
||
"Run `display-buffer' on BUF.
|
||
Expand `denote-link-backlinks-display-buffer-action'."
|
||
(display-buffer
|
||
buf
|
||
`(,@denote-link-backlinks-display-buffer-action)))
|
||
|
||
(defun denote-backlinks-next (n)
|
||
"Use appropriate command for forward motion in backlinks buffer.
|
||
With N as a numeric argument, move to the Nth button from point.
|
||
A nil value of N is understood as 1.
|
||
|
||
When `denote-backlinks-show-context' is nil, move between files
|
||
in the backlinks buffer.
|
||
|
||
When `denote-backlinks-show-context' is non-nil move between
|
||
matching identifiers."
|
||
(interactive "p" denote-backlinks-mode)
|
||
(unless (derived-mode-p 'denote-backlinks-mode)
|
||
(user-error "Only use this in a Denote backlinks buffer"))
|
||
(if denote-backlinks-show-context
|
||
(xref-next-line)
|
||
(forward-button n)))
|
||
|
||
(defun denote-backlinks-prev (n)
|
||
"Use appropriate command for backward motion in backlinks buffer.
|
||
With N as a numeric argument, move to the Nth button from point.
|
||
A nil value of N is understood as 1.
|
||
|
||
When `denote-backlinks-show-context' is nil, move between files
|
||
in the backlinks buffer.
|
||
|
||
When `denote-backlinks-show-context' is non-nil move between
|
||
matching identifiers."
|
||
(interactive "p" denote-backlinks-mode)
|
||
(unless (derived-mode-p 'denote-backlinks-mode)
|
||
(user-error "Only use this in a Denote backlinks buffer"))
|
||
(if denote-backlinks-show-context
|
||
(xref-prev-line)
|
||
(backward-button n)))
|
||
|
||
(defvar denote-backlinks-mode-map
|
||
(let ((m (make-sparse-keymap)))
|
||
(define-key m "n" #'denote-backlinks-next)
|
||
(define-key m "p" #'denote-backlinks-prev)
|
||
(define-key m "g" #'revert-buffer)
|
||
m)
|
||
"Keymap for `denote-backlinks-mode'.")
|
||
|
||
(define-derived-mode denote-backlinks-mode xref--xref-buffer-mode "Backlinks"
|
||
"Major mode for backlinks buffers."
|
||
(unless denote-backlinks-show-context
|
||
(font-lock-add-keywords nil denote-faces-file-name-keywords-for-backlinks t))
|
||
(add-hook 'project-find-functions #'denote-project-find nil t))
|
||
|
||
(defun denote-link--prepare-backlinks (fetcher _alist)
|
||
"Create backlinks' buffer for the current note.
|
||
FETCHER is a function that fetches a list of xrefs. It is called
|
||
with `funcall' with no argument like `xref--fetcher'.
|
||
|
||
In the case of `denote', `apply-partially' is used to create a
|
||
function that has already applied another function to multiple
|
||
arguments.
|
||
|
||
ALIST is not used in favour of using
|
||
`denote-link-backlinks-display-buffer-action'."
|
||
(let* ((inhibit-read-only t)
|
||
(file (buffer-file-name))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(id (denote-retrieve-filename-identifier file))
|
||
(buf (format "*denote-backlinks to %s*" id))
|
||
(xref-alist (xref--analyze (funcall fetcher)))
|
||
(dir (denote-directory)))
|
||
(with-current-buffer (get-buffer-create buf)
|
||
(setq-local default-directory dir)
|
||
(erase-buffer)
|
||
(setq overlay-arrow-position nil)
|
||
(denote-backlinks-mode)
|
||
(goto-char (point-min))
|
||
(when-let ((title (denote-retrieve-title-value file file-type))
|
||
(heading (format "Backlinks to %S (%s)" title id))
|
||
(l (length heading)))
|
||
(insert (format "%s\n%s\n\n" heading (make-string l ?-))))
|
||
(if denote-backlinks-show-context
|
||
(xref--insert-xrefs xref-alist)
|
||
(mapc (lambda (x)
|
||
(insert (car x))
|
||
(make-button (line-beginning-position) (line-end-position) :type 'denote-link-backlink-button)
|
||
(newline))
|
||
xref-alist))
|
||
(goto-char (point-min))
|
||
(setq-local revert-buffer-function
|
||
(lambda (_ignore-auto _noconfirm)
|
||
(when-let ((buffer-file-name file))
|
||
(denote-link--prepare-backlinks
|
||
(apply-partially #'xref-matches-in-files id
|
||
(delete file (denote-directory-text-only-files)))
|
||
nil)))))
|
||
(denote-link--display-buffer buf)))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-backlinks
|
||
'denote-backlinks
|
||
"2.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-backlinks ()
|
||
"Produce a buffer with backlinks to the current note.
|
||
|
||
The backlinks' buffer shows the file name of the note linking to
|
||
the current note, as well as the context of each link.
|
||
|
||
File names are fontified by Denote if the user option
|
||
`denote-link-fontify-backlinks' is non-nil. If this user option
|
||
is nil, the buffer is fontified by Xref.
|
||
|
||
The placement of the backlinks' buffer is controlled by the user
|
||
option `denote-link-backlinks-display-buffer-action'. By
|
||
default, it will show up below the current window."
|
||
(interactive)
|
||
(let ((file (buffer-file-name)))
|
||
(when (denote-file-is-writable-and-supported-p file)
|
||
(let* ((id (denote-retrieve-filename-identifier file))
|
||
(xref-show-xrefs-function #'denote-link--prepare-backlinks)
|
||
(project-find-functions #'denote-project-find))
|
||
(xref--show-xrefs
|
||
(apply-partially #'xref-matches-in-files id
|
||
;; remove the current buffer file from the
|
||
;; backlinks
|
||
(delete file (denote-directory-text-only-files)))
|
||
nil)))))
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-show-backlinks-buffer
|
||
'denote-show-backlinks-buffer
|
||
"2.0.0")
|
||
|
||
(defalias 'denote-show-backlinks-buffer 'denote-backlinks
|
||
"Alias for `denote-backlinks' command.")
|
||
|
||
;;;;; Add links matching regexp
|
||
|
||
(defvar denote-link--prepare-links-format "- %s\n"
|
||
"Format specifiers for `denote-link-add-links'.")
|
||
|
||
;; NOTE 2022-06-16: There is no need to overwhelm the user with options,
|
||
;; though I expect someone to want to change the sort order.
|
||
(defvar denote-link-add-links-sort nil
|
||
"When t, add REVERSE to `sort-lines' of `denote-link-add-links'.")
|
||
|
||
(defun denote-link--prepare-links (files current-file-type id-only)
|
||
"Prepare links to FILES from CURRENT-FILE-TYPE.
|
||
When ID-ONLY is non-nil, use a generic link format. See
|
||
`denote-link--file-type-format'."
|
||
(with-temp-buffer
|
||
(mapc
|
||
(lambda (file)
|
||
(insert
|
||
(format
|
||
denote-link--prepare-links-format
|
||
(denote-format-link
|
||
file
|
||
(denote-link--file-type-format current-file-type id-only)
|
||
(denote--link-get-description file (denote-filetype-heuristics file))))))
|
||
files)
|
||
(sort-lines denote-link-add-links-sort (point-min) (point-max))
|
||
(buffer-string)))
|
||
|
||
(defvar denote-link--add-links-history nil
|
||
"Minibuffer history for `denote-add-links'.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-add-links
|
||
'denote-add-links
|
||
"2.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-add-links (regexp &optional id-only)
|
||
"Insert links to all notes matching 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."
|
||
(interactive
|
||
(list
|
||
(read-regexp "Insert links matching REGEX: " nil 'denote-link--add-links-history)
|
||
current-prefix-arg))
|
||
(let* ((current-file (buffer-file-name))
|
||
(file-type (denote-filetype-heuristics current-file)))
|
||
(if-let ((files (delete current-file
|
||
(denote-directory-files-matching-regexp regexp)))
|
||
(beg (point)))
|
||
(progn
|
||
(insert (denote-link--prepare-links files file-type id-only))
|
||
(denote-link-buttonize-buffer beg (point)))
|
||
(message "No links matching `%s'" regexp))))
|
||
|
||
(defalias 'denote-link-insert-links-matching-regexp 'denote-add-links
|
||
"Alias for `denote-add-links' command.")
|
||
|
||
(define-obsolete-function-alias
|
||
'denote-link-add-missing-links
|
||
'denote-add-missing-links
|
||
"2.0.0")
|
||
|
||
;;;###autoload
|
||
(defun denote-add-missing-links (regexp &optional id-only)
|
||
"Insert missing links to all notes matching REGEXP.
|
||
Similar to `denote-add-links' but insert only links not yet
|
||
present in the current buffer.
|
||
|
||
Optional ID-ONLY has the same meaning as in `denote-link': it
|
||
inserts links with just the identifier."
|
||
(interactive
|
||
(list
|
||
(read-regexp "Insert links matching REGEX: " nil 'denote-link--add-links-history)
|
||
current-prefix-arg))
|
||
(let* ((current-file (buffer-file-name))
|
||
(file-type (denote-filetype-heuristics current-file))
|
||
(current-id (denote--link-in-context-regexp file-type))
|
||
(linked-files (denote-link--expand-identifiers current-id)))
|
||
(if-let ((found-files (delete current-file
|
||
(denote-directory-files-matching-regexp regexp)))
|
||
(final-files (seq-difference found-files linked-files))
|
||
(beg (point)))
|
||
(progn
|
||
(insert (denote-link--prepare-links final-files file-type id-only))
|
||
(denote-link-buttonize-buffer beg (point)))
|
||
(message "No links matching `%s' that aren't yet present in the current buffer" regexp))))
|
||
|
||
;;;;; Links from Dired marks
|
||
|
||
;; NOTE 2022-07-21: I don't think we need a history for this one.
|
||
(defun denote-link--buffer-prompt (buffers)
|
||
"Select buffer from BUFFERS visiting Denote notes."
|
||
(let ((buffer-file-names (mapcar #'file-name-nondirectory
|
||
buffers)))
|
||
(completing-read
|
||
"Select note buffer: "
|
||
(denote--completion-table 'buffer buffer-file-names)
|
||
nil t)))
|
||
|
||
(defun denote-link--map-over-notes ()
|
||
"Return list of `denote-file-is-note-p' from Dired marked items."
|
||
(when (denote--dir-in-denote-directory-p default-directory)
|
||
(seq-filter #'denote-file-is-note-p (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 are Denote notes, meaning that they have our file-naming
|
||
scheme, are writable/regular files, and use the appropriate file
|
||
type extension (per `denote-file-type'). Furthermore, the marked
|
||
files need to be inside the variable `denote-directory' or one of
|
||
its subdirectories. No other file is recognised (the list of
|
||
marked files ignores whatever does not count as a note for our
|
||
purposes).
|
||
|
||
The BUFFER is one which visits a Denote note file. If there are
|
||
multiple buffers, prompt with completion for one among them. If
|
||
there isn't one, 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
|
||
(list
|
||
(denote-link--map-over-notes)
|
||
(let ((file-names (denote--buffer-file-names)))
|
||
(find-file
|
||
(cond
|
||
((null file-names)
|
||
(user-error "No buffers visiting Denote notes"))
|
||
((eq (length file-names) 1)
|
||
(car file-names))
|
||
(t
|
||
(denote-link--buffer-prompt file-names)))))
|
||
current-prefix-arg)
|
||
dired-mode)
|
||
(if (null files)
|
||
(user-error "No note files to link to")
|
||
(when (y-or-n-p (format "Create links at point in %s?" buffer))
|
||
(with-current-buffer buffer
|
||
(insert (denote-link--prepare-links
|
||
files
|
||
(denote-filetype-heuristics (buffer-file-name))
|
||
id-only))
|
||
(denote-link-buttonize-buffer)))))
|
||
|
||
;;;;; 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 link" denote-link
|
||
:help "Insert link to a file in the `denote-directory'"
|
||
:enable (derived-mode-p 'text-mode)]
|
||
["Insert links with regexp" denote-add-links
|
||
:help "Insert links to files matching regexp in the `denote-directory'"
|
||
: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)]
|
||
"---"
|
||
["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.")
|
||
|
||
(easy-menu-define denote-global-menu nil
|
||
"Menu with all Denote commands, each available in the right context."
|
||
denote--menu-contents)
|
||
|
||
;; Add Denote menu at the end of global-map after Tools
|
||
(easy-menu-add-item global-map '(menu-bar)
|
||
denote-global-menu)
|
||
|
||
(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 path-id)
|
||
"Resolve LINK into the appropriate target.
|
||
With optional PATH-ID return a cons cell consisting of the path
|
||
and the identifier."
|
||
(let* ((search (and (string-match "::\\(.*\\)\\'" link)
|
||
(match-string 1 link)))
|
||
(id (if (and (stringp search) (not (string-empty-p search)))
|
||
(substring link 0 (match-beginning 0))
|
||
link))
|
||
(path (denote-get-path-by-id id)))
|
||
(cond
|
||
(path-id
|
||
(cons (format "%s" path) (format "%s" id)))
|
||
((and (stringp search) (not (string-empty-p search)))
|
||
(concat path "::" search))
|
||
(path))))
|
||
|
||
;;;###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
|
||
search option akin to that of standard Org `file:' link types.
|
||
Read Info node `(org) Search Options'.
|
||
|
||
Uses the function `denote-directory' to establish the path to the
|
||
file."
|
||
(org-link-open-as-file
|
||
(denote-link--ol-resolve-link-to-target link)
|
||
nil))
|
||
|
||
;;;###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)))
|
||
(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)
|
||
|
||
;;;###autoload
|
||
(defun denote-link-ol-store ()
|
||
"Handler for `org-store-link' adding support for denote: links."
|
||
(when-let ((file (buffer-file-name))
|
||
((denote-file-is-note-p file))
|
||
(file-type (denote-filetype-heuristics file))
|
||
(file-id (denote-retrieve-filename-identifier file))
|
||
(file-title (denote--retrieve-title-or-filename file file-type)))
|
||
(org-link-store-props
|
||
:type "denote"
|
||
:description file-title
|
||
:link (concat "denote:" file-id))
|
||
org-store-link-plist))
|
||
|
||
;;;###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."
|
||
(let* ((path-id (denote-link--ol-resolve-link-to-target link :path-id))
|
||
(path (file-relative-name (car path-id)))
|
||
(p (file-name-sans-extension path))
|
||
(id (cdr path-id))
|
||
(desc (or description (concat "denote:" id))))
|
||
(cond
|
||
((eq format 'html) (format "<a href=\"%s.html\">%s</a>" p desc))
|
||
((eq format 'latex) (format "\\href{%s}{%s}" (replace-regexp-in-string "[\\{}$%&_#~^]" "\\\\\\&" path) desc))
|
||
((eq format 'texinfo) (format "@uref{%s,%s}" path desc))
|
||
((eq format 'ascii) (format "[%s] <denote:%s>" desc path)) ; NOTE 2022-06-16: May be tweaked further
|
||
((eq format 'md) (format "[%s](%s.md)" desc p))
|
||
(t path))))
|
||
|
||
;; 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"
|
||
:follow #'denote-link-ol-follow
|
||
:face 'denote-faces-link
|
||
: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)
|
||
|
||
(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.
|
||
|
||
Note that this function ignores the `denote-file-type': 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."
|
||
(let* ((title (denote-title-prompt))
|
||
(keywords (denote-keywords-prompt))
|
||
(front-matter (denote--format-front-matter
|
||
title (denote--date nil 'org) keywords
|
||
(format-time-string denote-id-format nil) 'org)))
|
||
(setq denote-last-path
|
||
(denote--path title keywords
|
||
(file-name-as-directory (denote-directory))
|
||
(format-time-string denote-id-format) 'org))
|
||
(denote--keywords-add-to-history keywords)
|
||
(concat front-matter denote-org-capture-specifiers)))
|
||
|
||
;;;###autoload
|
||
(defun denote-org-capture-with-prompts (&optional title keywords subdirectory date template)
|
||
"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, and
|
||
TEMPLATE 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* ((title (if title (denote-title-prompt) ""))
|
||
(kws (if keywords (denote-keywords-prompt) nil))
|
||
(directory (file-name-as-directory (if subdirectory (denote-subdirectory-prompt) (denote-directory))))
|
||
(date (if date (denote--valid-date (denote-date-prompt)) (current-time)))
|
||
(id (denote--find-first-unused-id
|
||
(format-time-string denote-id-format date)
|
||
(denote--get-all-used-ids)))
|
||
(template (if template (denote-template-prompt) ""))
|
||
(front-matter (denote--format-front-matter
|
||
title (denote--date date 'org) kws
|
||
(format-time-string denote-id-format date) 'org)))
|
||
(setq denote-last-path
|
||
(denote--path title kws directory id 'org))
|
||
(denote--keywords-add-to-history kws)
|
||
(concat front-matter template denote-org-capture-specifiers)))
|
||
|
||
(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)
|
||
|
||
;;;; Denote extension "modules"
|
||
|
||
(defvar denote-modules-available
|
||
'(project (project-find-functions . denote-project-find)
|
||
xref (xref-backend-functions . denote--xref-backend)
|
||
ffap (denote-module-ffap-enable . denote-module-ffap-disable))
|
||
"Denote modules currently built-in with Denote.
|
||
This variable is a plist. Each module is represented as a pair
|
||
of a property name and its value being a cons cell; thus a module
|
||
is written in either the following forms:
|
||
|
||
NAME (HOOK . FUNCTION\)
|
||
NAME (FUNCTION . FUNCTION\)
|
||
|
||
NAME, HOOK, FUNCTION are symbols.
|
||
|
||
When a HOOK-FUNCTION pair is used, `denote-modules-enable'
|
||
function will add FUNCTION to HOOK and `denote-modules-disable'
|
||
function will remove FUNCTION from HOOK. Generally, it should be
|
||
possible to set HOOK-FUNCTION modules locally.
|
||
|
||
When a FUNCTION-FUNCTION pair is used, the first FUNCTION must be
|
||
an enable function and the second, its corresponding disable
|
||
function to undo the former. They are both called with no
|
||
arguments. For FUNCTION-FUNCTION modules, in some cases, it may
|
||
not be possible to enable a module locally. In these cases, some
|
||
parts of a module may be enabled globally even when local minor
|
||
mode function `denote-modules-mode' is called.
|
||
|
||
NOTES for future development to add new modules:
|
||
|
||
It is important that FUNCTION must be defined and loaded before
|
||
`denote-modules-enable' and `denote-moduel-disable' (the new
|
||
functions probably should be written in the source code lines
|
||
before these enable/disable functions)")
|
||
|
||
(defvar denote-module-ffap-last-enabled nil
|
||
"Value of `ffap-next-regexp' beofe ffap module was last enabled.
|
||
It is used by `denote-module-ffap-disable' to undo the value
|
||
the module previoulsy set.")
|
||
|
||
(defvar denote-modules-last-enabled nil
|
||
"Denote modules set last time.
|
||
It is used by `denote-modules-enable' and
|
||
`denote-moduules-disable' to undo the modules enabled last time.")
|
||
|
||
;; defvars to placate the compilers
|
||
(defvar denote-modules)
|
||
(defvar ffap-next-regexp)
|
||
(defvar ffap-alist)
|
||
|
||
(defun denote-module-ffap-disable (&optional local)
|
||
"Disable Denote integration with `ffap'.
|
||
This function is meant to be set as a pair function with
|
||
`denote-module-ffap-enable' in `denote-modules-available'.
|
||
|
||
When LOCAL is non-nil, enable only for the local buffer as
|
||
much as possible. Currently, `ffap-alist' is only disabled
|
||
globally."
|
||
(require 'ffap)
|
||
(setq ffap-alist (rassq-delete-all #'denote-get-relative-path-by-id ffap-alist))
|
||
(if local
|
||
(when denote-module-ffap-last-enabled
|
||
(setq-local ffap-next-regexp denote-module-ffap-last-enabled))
|
||
;; Reset `ffap-next-regexp' only when there is last-active. Nil
|
||
;; means it is in the loading process of denote
|
||
(when denote-module-ffap-last-enabled
|
||
(setq ffap-next-regexp denote-module-ffap-last-enabled))))
|
||
|
||
(defun denote-module-ffap-enable (&optional local)
|
||
"Enable Denote integration with `ffap'.
|
||
This function is meant to be set as a pair function with
|
||
`denote-module-ffap-disable' in `denote-modules-available'.
|
||
|
||
When LOCAL is non-nil, enable only for the local buffer as much
|
||
as possible. Currently, `'ffap-alist' is only enabled globally."
|
||
(require 'ffap)
|
||
(if local (setq-local denote-module-ffap-last-active ffap-next-regexp)
|
||
(setq denote-module-ffap-last-enabled ffap-next-regexp)
|
||
(add-to-list 'ffap-alist (cons denote-id-regexp #'denote-get-relative-path-by-id)))
|
||
(if local
|
||
(setq-local ffap-next-regexp (concat ffap-next-regexp "\\|" denote-id-regexp))
|
||
(setq ffap-next-regexp (concat ffap-next-regexp "\\|" denote-id-regexp))))
|
||
|
||
(defun denote-modules-disable (modules &optional local)
|
||
"Disable Denote integration MODULES.
|
||
This function is meant to be used by `denote-modules-enable',
|
||
which calls this function, passgin `denote-modules-last-enable'
|
||
as MODULES to undo the modules currently active.
|
||
|
||
When LOCAL is non-nil, disable MODULES locally, where possible.
|
||
|
||
Refer to document string of `denote-modules-available'."
|
||
(dolist (module modules)
|
||
(let* ((module-def (plist-get denote-modules-available module))
|
||
(hook (car module-def))
|
||
(func (cdr module-def)))
|
||
;; If HOOK is a function, it's a setup function and FUNC is its
|
||
;; teardown counterpart.
|
||
(if (functionp hook) (funcall func local)
|
||
(remove-hook hook func local)))))
|
||
|
||
(defun denote-modules-enable (modules &optional local)
|
||
"Enable MODULES set in `denote-modules'.
|
||
When LOCAL is non-nil, it tries to enable them only locally.
|
||
Whether this is possible or not depends on the module in
|
||
question.
|
||
|
||
Refer to document string of `denote-modules-available'."
|
||
(denote-modules-disable denote-modules-last-enabled)
|
||
(dolist (module modules)
|
||
(let* ((module-def (plist-get denote-modules-available module))
|
||
(hook (car module-def))
|
||
(func (cdr module-def)))
|
||
;; If HOOK is a function, it's a setup function and FUNC is its
|
||
;; teardown counterpart.
|
||
(if (functionp hook) (funcall hook local)
|
||
(add-hook hook func nil local))))
|
||
(if local (setq denote-modules-last-enabled modules)
|
||
(setq denote-modules-last-enabled modules)))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode denote-modules-mode
|
||
"Enable Denote integration modules locally.
|
||
Set modules to be enabled in `denote-modules' and activate the
|
||
minor mode, either globally or locally. The selected modules are
|
||
enabled only when the minor mode is active."
|
||
:global nil
|
||
:init-value nil
|
||
(if denote-modules-mode
|
||
(denote-modules-enable denote-modules :local)
|
||
(denote-modules-disable denote-modules-last-enabled :local)))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode denote-modules-global-mode
|
||
"Enable Denote integration modules globally.
|
||
Set modules to be enabled in `denote-modules' and activate the
|
||
minor mode, either globally or locally. The selected modules are
|
||
enabled only when the minor mode is active."
|
||
:global t
|
||
:init-value nil
|
||
(if denote-modules-global-mode
|
||
(denote-modules-enable denote-modules)
|
||
(denote-modules-disable denote-modules-last-enabled)))
|
||
|
||
(defun denote-modules-set (symbol value)
|
||
"Set SYMBOL and VALUE for `denote-modules' upon customizing.
|
||
Enable the modules set when `denote-modules-mode' or
|
||
`denote-modules-global-mode' is active. If not, this function
|
||
does not enable them automatically. Manually call the minor mode
|
||
globally or locally or set it in your configuration.
|
||
|
||
It is meant to be used `defcustom' of `denote-modules', thus when
|
||
the minor mode is active, changing the modules in the `customize'
|
||
UI will be effective immediately."
|
||
(set symbol value)
|
||
(when (or denote-modules-global-mode denote-modules-mode)
|
||
(denote-modules-enable value)))
|
||
|
||
(defcustom denote-modules nil
|
||
"User-selected Denote modules.
|
||
The selected modules are a list of NAME (symbols), and each
|
||
module enables integration with another Emacs built-in feature.
|
||
See `denote-modules-available' for the modules currently
|
||
available. Set this user option as a list of NAME; for example:
|
||
|
||
(project xref ffap)
|
||
|
||
When customized in Customize UI, it presents a set of checkboxes,
|
||
each box checked adds NAME of the module to the list.
|
||
|
||
Modules are automatically enabled only when either
|
||
`denote-modules-mode' or `denote-modules-global-mode' is active.
|
||
If not, setting the modules does not enable or disable them
|
||
automatically. Manually call the minor mode globally or locally
|
||
or set it in your configuration."
|
||
:group 'denote
|
||
:set #'denote-modules-set
|
||
:package-version '(denote . "1.2.0")
|
||
:type
|
||
'(set (const :tag "Project integration" project)
|
||
(const :tag "Xref integration " xref)
|
||
(const :tag "Integration with find-file-at-point `ffap'" ffap)))
|
||
|
||
;;;; project.el integration
|
||
;; This is also used by xref integration
|
||
|
||
(cl-defmethod project-root ((project (head denote)))
|
||
"Denote's implementation of `project-root' method from `project'.
|
||
Return current variable `denote-directory' as the root of the
|
||
current denote PROJECT."
|
||
(cdr project))
|
||
|
||
(cl-defmethod project-files ((_project (head denote)) &optional _dirs)
|
||
"Denote's implementation of `project-files' method from `project'.
|
||
Return all files that have an identifier for the current denote
|
||
PROJECT. The return value may thus include file types that are
|
||
not implied by `denote-file-type'. To limit the return value to
|
||
text files, use the function `denote-directory-text-only-files'."
|
||
(denote-directory-files))
|
||
|
||
(defun denote-project-find (dir)
|
||
"Return project instance if DIR is part of variable `denote-directory'.
|
||
The format of project instance is aligned with `project-try-vc'
|
||
defined in `project'."
|
||
(let ((dir (expand-file-name dir)) ; canonicalize current directory name
|
||
(root (denote-directory)))
|
||
(when (or (file-equal-p dir root) ; currently at `denote-directory'
|
||
(string-prefix-p root dir)) ; or its subdirectory
|
||
(cons 'denote root))))
|
||
|
||
;;;; Xref integration
|
||
;; Set `xref-backend-functions' like this.
|
||
;; (add-hook 'xref-backend-functions #'denote--xref-backend)
|
||
;;
|
||
;; You can tell xref-references not to prompt by adding the following:
|
||
;; (add-to-list 'xref-prompt-for-identifier #'xref-find-references
|
||
;; :append)
|
||
|
||
(defun denote--xref-backend ()
|
||
"Return denote if `default-directory' is in denote directory."
|
||
(when (denote--dir-in-denote-directory-p default-directory)
|
||
'denote))
|
||
|
||
(cl-defmethod xref-backend-identifier-at-point ((_backend (eql 'denote)))
|
||
"Return the \"thing\" at point.
|
||
The same logic as `elisp-mode'. The \"thing\" is assumed to be a
|
||
Denote identifier, but can be any word. The method checks this
|
||
and errors and if the word at point is not a Denote identifier."
|
||
(let ((bounds (bounds-of-thing-at-point 'word)))
|
||
(and bounds
|
||
(let ((id (buffer-substring-no-properties
|
||
(car bounds) (cdr bounds))))
|
||
(if (string-match-p denote-id-regexp id)
|
||
;; Use a property to transport the location of the identifier.
|
||
(propertize id 'pos (car bounds))
|
||
(user-error "%s is not a Denote identifier" id))))))
|
||
|
||
(cl-defmethod xref-backend-definitions ((_backend (eql 'denote)) identifier)
|
||
"Return xref for the note IDENTIFIER points to."
|
||
(let ((file (denote-get-path-by-id identifier)))
|
||
(when file
|
||
(if (file-equal-p file (buffer-file-name (current-buffer)))
|
||
(user-error "Identifier points to the current buffer")
|
||
;; Without the message, Xref will report that the ID does not
|
||
;; exist, which is incorrect in this case.
|
||
(list (xref-make nil (xref-make-file-location file 0 0)))))))
|
||
|
||
(cl-defmethod xref-backend-references ((_backend (eql 'denote)) identifier)
|
||
"Return list of xrefs where IDENTIFIER is referenced.
|
||
This include the definition itself."
|
||
(xref-matches-in-files identifier (denote-directory-text-only-files)))
|
||
|
||
(cl-defmethod xref-backend-identifier-completion-table ((_backend
|
||
(eql 'denote)))
|
||
"Return list of Denote identifers as completion table."
|
||
|
||
(mapcar #'denote-retrieve-filename-identifier (denote-all-files)))
|
||
|
||
(provide 'denote)
|
||
;;; denote.el ends here
|