Honor unapplied dir-local denote-directory in denote-directories

Directory-local variables are applied only after the major mode body
has run: 'run-mode-hooks' calls 'hack-local-variables' after the mode
hooks.  Anything that resolves Denote paths while the mode is still
initializing therefore sees the global 'denote-directory' instead of
the silo's dir-local value.  Org's startup link previews
('org-startup-with-link-previews') are one such case: previews of
'denote:' links inside a silo resolve against the wrong directory.

Make 'denote-directories' read the dir-local value directly from the
directory-local variables data in that window.  A buffer-local value
or an explicit dynamic binding of 'denote-directory' still takes
precedence, preserving the documented let-binding override.

The read uses 'hack-dir-local--get-variables', which is available in
Emacs 30 or higher; older versions behave as before.  The value is
read without the safe-local-variable filter, which is equivalent for
this variable because its 'safe-local-variable' predicate accepts any
string or list value.
This commit is contained in:
Le Wang 2026-07-15 00:21:48 -04:00
parent 1004f735b7
commit 2fe3bcdf68
No known key found for this signature in database
GPG key ID: 784BC96BE3A994DD
2 changed files with 62 additions and 11 deletions

View file

@ -961,25 +961,50 @@ Make any parent directories as well."
(mapcar get-dir directory-or-directories)
(list (funcall get-dir directory-or-directories)))))
(declare-function hack-dir-local--get-variables "files" (&optional predicate))
(defun denote-directories--dir-local-value ()
"Return the dir-local value of variable `denote-directory', if any.
Read the value directly from the directory-local variables data, so
that it is available even before `hack-local-variables' has applied it.
Major mode bodies run before directory-local variables are applied to
the buffer, so anything that resolves Denote paths during mode setup,
such as Org link previews at startup, would otherwise see the wrong
value inside a silo.
Return nil if the variable `denote-directory' is buffer-local or
dynamically bound, if directory-local variables are disabled, or if
they do not specify a value for the variable `denote-directory'."
(when (and (not (local-variable-p 'denote-directory))
(eq denote-directory (default-toplevel-value 'denote-directory))
(fboundp 'hack-dir-local--get-variables))
(cdr (assq 'denote-directory
(cdr (hack-dir-local--get-variables))))))
(defun denote-directories ()
"Return path of variable `denote-directory' as a proper directory.
If the variable `denote-directory' is set to a list of file paths,
return the list with each element expanded to be a directory. Create
any directories and their parents, if needed.
A directory-local value of the variable `denote-directory' is honored
even when it has not yet been applied to the buffer, as is the case
while the major mode is still initializing.
Custom Lisp code can `let' bind the variable `denote-directory'
to override what this function returns."
(if-let* (((or (eq denote-directory 'default-directory) (eq denote-directory 'local)))
(silo-dir (denote--default-directory-is-silo-p)))
(progn
(display-warning
'denote
"Silo value must be a string; `local' or `default-directory' are obsolete"
:error)
(list silo-dir))
(let ((denote-directories (denote-directories--get-paths denote-directory)))
(denote-directories--make-paths denote-directories)
denote-directories)))
(let ((denote-directory (or (denote-directories--dir-local-value) denote-directory)))
(if-let* (((or (eq denote-directory 'default-directory) (eq denote-directory 'local)))
(silo-dir (denote--default-directory-is-silo-p)))
(progn
(display-warning
'denote
"Silo value must be a string; `local' or `default-directory' are obsolete"
:error)
(list silo-dir))
(let ((denote-directories (denote-directories--get-paths denote-directory)))
(denote-directories--make-paths denote-directories)
denote-directories))))
(defun denote-has-single-denote-directory-p ()
"Return non-nil if the variable `denote-directory' is a single item."

View file

@ -95,6 +95,32 @@ Also see `denote-test--denote--make-denote-directory',
(should (and (seq-every-p #'file-directory-p denote-directory)
(seq-every-p #'file-name-absolute-p denote-directory)))))
(ert-deftest dt-denote-directories--dir-local-value ()
"Test that an unapplied dir-local `denote-directory' is honored.
Directory-local variables are applied only after the major mode is
initialized, so `denote-directories' reads the dir-local value directly
in that window. A dynamic binding or a buffer-local value of the
variable `denote-directory' still takes precedence."
(skip-unless (fboundp 'hack-dir-local--get-variables))
(let* ((directory (file-name-as-directory
(expand-file-name "denote-test-silo" temporary-file-directory)))
(silo (file-name-as-directory (expand-file-name "notes" directory))))
(unwind-protect
(progn
(make-directory directory :parents)
(with-temp-file (expand-file-name ".dir-locals.el" directory)
(insert (format "((nil . ((denote-directory . %S))))" silo)))
(with-temp-buffer
(setq default-directory directory)
(should (equal (denote-directories) (list silo)))
(let ((denote-directory "/tmp/denote-test-notes"))
(should (equal (denote-directories)
(denote-directories--get-paths denote-directory))))
(setq-local denote-directory "/tmp/denote-test-notes")
(should (equal (denote-directories)
(denote-directories--get-paths denote-directory)))))
(delete-directory directory :recursive))))
(ert-deftest dt-denote-sluggify-title ()
"Test that `denote-sluggify-title' removes punctuation from the string.
Concretely, remove anything specified in `denote-sluggify-title'."