From 2fe3bcdf68df7d5bcf19cb670896363955546771 Mon Sep 17 00:00:00 2001 From: Le Wang Date: Wed, 15 Jul 2026 00:21:48 -0400 Subject: [PATCH] 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. --- denote.el | 47 +++++++++++++++++++++++++++++++++----------- tests/denote-test.el | 26 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/denote.el b/denote.el index b0fa0b2..6ec8e6b 100644 --- a/denote.el +++ b/denote.el @@ -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." diff --git a/tests/denote-test.el b/tests/denote-test.el index 7a222d3..7c81cac 100644 --- a/tests/denote-test.el +++ b/tests/denote-test.el @@ -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'."