Skip to content

Fix denote: link previews during Org mode startup in silos - #715

Open
lewang wants to merge 2 commits into
protesilaos:mainfrom
lewang:main
Open

Fix denote: link previews during Org mode startup in silos#715
lewang wants to merge 2 commits into
protesilaos:mainfrom
lewang:main

Conversation

@lewang

@lewang lewang commented Jul 15, 2026

Copy link
Copy Markdown

Follow-up to the :preview support from #317. With
org-startup-with-link-previews enabled, previews of denote: links
fail in two ways when the notes live in a silo (a dir-local
denote-directory).

Problem

  • Major mode bodies run before directory-local variables are applied:
    run-mode-hooks calls hack-local-variables only after the mode
    hooks. Org renders startup link previews inside the org-mode body,
    so denote-directories resolves identifiers against the global
    denote-directory instead of the silo's dir-local value.
  • When an identifier does not resolve, denote-link-preview-file
    passes nil to org-link-preview-file, which signals
    wrong-type-argument. During startup previews this aborts the
    entire org-mode setup, leaving the buffer half-initialized with
    directory-local variables never applied.

Changes

  • denote-directories now honors a dir-local denote-directory that
    has not yet been applied to the buffer. A new helper,
    denote-directories--dir-local-value, reads the value from the
    directory-local variables data on demand.
  • denote-link-preview-file skips the preview when the target does
    not resolve, instead of erroring.

Technical Details

  • The on-demand read uses hack-dir-local--get-variables, available
    in Emacs 30 or higher; the call is fboundp-guarded, so older
    versions behave exactly as before.
  • A buffer-local value or an explicit dynamic binding of
    denote-directory still takes precedence, preserving the documented
    let-binding override of denote-directories.
  • The value is read without the safe-local-variable filter, which is
    equivalent for this variable: its safe-local-variable predicate
    accepts any string or list value.

Testing

  • New ERT tests in tests/denote-test.el: the unapplied dir-local
    value is honored; a dynamic binding and a buffer-local value each
    take precedence; an unresolvable preview target returns nil without
    error.
  • Verified interactively on Emacs 31 with Org 9.8.7: a silo buffer
    with org-startup-with-link-previews now initializes fully and
    renders denote: image previews.

Le Wang added 2 commits July 15, 2026 00:21
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-link-preview-file passed the resolved link target straight to
org-link-preview-file.  When the identifier does not resolve to a
file, that target is nil and org-link-preview-file signals
wrong-type-argument.  With link previews enabled at startup
('org-startup-with-link-previews'), the error aborts the entire
org-mode setup, leaving the buffer half-initialized with
directory-local variables never applied.

Skip the preview instead when the target does not resolve.
@protesilaos

Copy link
Copy Markdown
Owner

Thank you @lewang for doing this! For some reason I did not receive the notification and am only noticing this now.

Major mode bodies run before directory-local variables are applied: run-mode-hooks calls hack-local-variables only after the mode hooks. Org renders startup link previews inside the org-mode body, so denote-directories resolves identifiers against the global denote-directory instead of the silo's dir-local value.

I remember having such a problem one time, which had to do with my dired-preview package---I still need to fix that there. But otherwise I cannot reproduce this problem. Does it happen consistently on your end? Maybe you can show me a recipe to reproduce this inside of emacs -Q?

Otherwise, yes, I am keen on fixing this problem.

@lewang

lewang commented Jul 29, 2026

Copy link
Copy Markdown
Author

No trouble at all about the notification. Thank you for the
encouragement, and for Denote itself -- the silo design is what made my
notes tractable in the first place.

Just a note that the repro below is generated with AI assistance, but the script
is verified by me (human).

Yes, it happens consistently: the unpatched build failed on every run and
never once came up clean.

Two things hide it:

  • It leaves almost no trace. normal-mode wraps its set-auto-mode
    call in its own condition-case, so nothing reaches the debugger and
    there is no backtrace -- just one echo-area line, File mode specification error: (wrong-type-argument stringp nil). The buffer
    still ends up in org-mode and still fontifies, so nothing looks
    broken. The damage is silent: the error aborts the org-mode body
    before run-mode-hooks, so hack-local-variables never runs and the
    silo's directory-local variables are never applied. What a user
    notices later is Denote quietly operating on the global
    denote-directory instead of the silo's.
  • It needs a graphical frame. org-link-preview-file opens with
    (when (display-graphic-p) ...), so under --batch the preview is
    never attempted and the run looks perfectly clean.

It also needs Org >= 9.8, where the :preview parameter shipped.
Released Emacs still bundles 9.7.x, in which denote-link-preview-file is
never called at all.

Recipe

Hands-off version, which pins Org from GNU ELPA into a throwaway init
directory and touches nothing under $HOME
(gist):

sh repro-715.sh /path/to/denote

By hand:

mkdir -p /tmp/silo /tmp/elsewhere
printf '((nil . ((denote-directory . "/tmp/silo"))))\n' > /tmp/silo/.dir-locals.el
cp any-image.png /tmp/silo/20260101T000001--pic__test.png

cat > /tmp/silo/20260101T000000--note__test.org <<'EOF'
#+title:      note
#+identifier: 20260101T000000

[[denote:20260101T000001]]
EOF

emacs -Q -L /path/to/denote
M-: (progn (setq-default denote-directory "/tmp/elsewhere")
           (require 'denote)
           (setq org-startup-with-link-previews t))

C-x C-f /tmp/silo/20260101T000000--note__test.org

Then: the echo area flashes the error (C-h e if it has scrolled past),
C-h v denote-directory still reads /tmp/elsewhere, and no preview
appears. The global value only has to be somewhere that does not contain
the identifier; the empty scratch directory is just so nothing can be
created under $HOME.

Cause

denote-link-preview-file                 target "20260101T000001"
denote-link--ol-resolve-link-to-target => (nil "20260101T000001" nil)
org-link-preview-file                    path=nil => wrong-type-argument

denote-directories is consulted from inside the org-mode body, before
the dir-local denote-directory has been applied, so the identifier is
looked up in the global directory and does not resolve.

The two commits

Tested separately, since either alone leaves something broken:

denote error dir-locals applied preview
upstream 1004f73 yes no no
+ d9d1257 no yes no
+ 2fe3bcd no yes yes
both, i.e. this PR no yes yes

d9d1257 alone stops the abort, so org-mode finishes and the dir-locals
do get applied, but the preview still silently does not render. 2fe3bcd
is what makes previews work inside a silo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants