This manual, written by Lucas Quintana, describes the customization
options for the Emacs package called denote-search
(or
denote-search.el
), and provides every other piece of information
pertinent to it.
The documentation furnished herein corresponds to stable version {{{stable-version}}}, released on {{{release-date}}}.
Copyright (C) 2024-2025 Lucas Quintana
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled “GNU Free Documentation License.”
This package provides a simple search utility for the excellent
denote
program, the simple-to-use, focused-in-scope, and effective
note-taking tool for Emacs (see info:denote#Top).
denote-search
allows you to search for a regular expression in the
contents of your notes. Once the results are populated, you are
presented with a buffer from which you can refine the search (that is,
search in the matched files), jump to a certain file using imenu,
exclude certain files from the search, or search only certain files.
All commands provided by Xref are available as well (see
info:emacs#Xref).
denote-search
approach has several advantages over similar tools
(consult-grep
, consult-notes
, grep
, and so on):
- It allows to search in the files matched by a previous search, which as far as I know is not possible with other similar packages.
- It allows to search in the files referenced in a region, with the
command
denote-search-files-referenced-in-region
. That is great for metanotes, shell outputs ofls
, and more. - It also allows to search in marked Dired files, with the command
denote-search-marked-dired-files
. - It doesn’t rely on the minibuffer to output results, and it thus
doesn’t need a completion stack in order to work (
helm
,vertico+consult
, etc.). - In fact,
denote-search
doesn’t rely on any external package other than Denote. - Unlike
grep
, it allows excluding (or including only) certain files on-demand, without command-line gimmicks. - It uses pretty file titles by default (and can be customized to show keywords or basically any other information as well).
Furthermore, denote-search
has a small and simple codebase. It is
designed to do one thing and one thing only: to search in your notes.
I wrote denote-search
because I needed a simple tool to search my
personal knowledge database, which of course is managed with Denote.
Available options, such as consult-notes
, almost always used the
minibuffer to display results. Which is fine, it works, but it is
sort of annoying on small screens. In fact, I hacked the first
version of denote-search
on Emacs for Android, in a time where I
didn’t have a laptop with me. Don’t take those things as granted,
trust me.
I was mainly inspired by Howm[fn:1], a really great note-taking tool,
also made for Emacs. It has a life-changing approach to notes: it
doesn’t matter where do you store your information as long as you can
retrieve it later. “Write fragmentarily and read collectively”, that
is. Howm has thus powerful search facilities built-in, some of which
(filtering and excluding/including files) I reimplemented in
denote-search
. I’m very happy now ^_^
The main point of entry of this package is the denote-search
command. This command will prompt for a string. You should input a
valid regular expression, as understood by the tool which will
actually perform the search. Which program that is depends on the
value of xref-search-program
, and its arguments are taken from
xref-search-program-alist
. Note that denote-search
is really just
a wrapper for Xref, albeit an useful one, so anything related to the
actual search results is the matter (for bad or for good) of that
library and/or the program it calls.
Once the (synchronous) search is over, a new buffer populated with the results, if any, will be made current. On that buffer, the user will be able to perform several potentially useful actions, including filtering the output (see filtering the search results) and searching in the matched files (see focused search).
History is available when searching. Press M-p
(previous-history-element
) to view past queries.
There are two additional commands that can start a search:
denote-search-marked-dired-files
and
denote-search-files-referenced-in-region
. They allow searching a
restricted subset of files and are described in the next sections.
The command denote-search-marked-dired-files
acts just like
denote-search
, but it restricts the search to the files marked in
current Dired buffer (see Marks vs Flags). This is useful if you only
want to search some files, though depending on the case maybe you’d
be better served by our built-in filtering capabilities; see
filtering the search results.
This works well in tandem with the Denote command denote-sort-dired
,
which produces a Dired buffer with files matching a regexp. So,
generating that buffer and then pressing t
(dired-toggle-marks
)
will enable you to use denote-search-marked-dired-files
to search on
those files. Again, using the filtering functionality available for
the results buffer should suit you better, but you have options.
The command denote-search-files-referenced-in-region
may seem odd at
first, but it’s probably the most useful one. It allows you to search
in a set of notes referenced in a buffer. What does that means?
Well, it means that any buffer can serve as the source for the set of
files to search for; you just need Denote IDs written somewhere, and
the command will recognize them as files and search in them.
But let’s look at an example. Probably, you already have a note with a section that resembles this:
* See also
- An amazing note
- Another amazing note
- Yet another amazing note
Those notes are links and are highlighted as such, so internally they look like this:
* See also
- [[denote:20231205T202124][An amazing note]]
- [[denote:20230720T154224][Another amazing note]]
- [[denote:20230719T194132][Yet another amazing note]]
That 20231205T202124
bit is the Denote ID. That’s the only thing
our command needs to recognize a note. So, you just need to select
the section (with the mouse or C-SPC
, whichever you like the most)
and call denote-search-files-referenced-in-region
. It will prompt
for a regexp just like denote-search
, but it will only search the
files selected.
This is useful for searching in notes linked in Org dynamic blocks
(first mark the block with org-babel-mark-block
), or for searching
something in linked notes in general (first mark the whole buffer with
mark-whole-buffer
, bound to C-x h
).
This works everywhere. If you had notes with the exact same IDs as the ones depicted previously, you could select them in this very same Info buffer (assuming you are reading this in Emacs) and search something in them right away.
As the Denote ID is included in file names, you can also use this
command on Dired, on a shell output of ls from async-shell-command
,
and so on. It’s on those cases where you can grasp how powerful the
Denote file-naming scheme is.
To navigate the results buffer, you can use the standard Xref commands
(see Xref Commands). So, for instance, n
moves you to the next hit
and displays it in another window, and p
does the same for the
previous one. N
moves you to the next file, while P
moves you to
the previous one.
Aside from that, denote-search
by default enables
outline-minor-mode
in the results buffer, and so additional
facilities are available. You can fold file matches with TAB
(outline-cycle
), in a similar fashion as an Org tree. You can also
navigate all the files using imenu
or an enhanced front-end such as
consult-imenu
. consult-outline
also works here.
You can press a
(outline-cycle-buffer
) to fold all files. This
can give you a quick overlook of all the files that matched the
search. If there are many, you can proceed to filter the output
(see filtering the search results).
On the results buffer, you can press x
(denote-search-exclude-files
) to exclude certain files from the
search. The command will prompt for a regular expression. Once
given, the last search query will be re-run, but excluding all the
files that match the regular expression given. For instance, you can
input _philosophy
to exclude all the notes with the “philosophy”
keyword. Or you can input -emacs
to exclude all the notes that have
“emacs” in their title. Or you can input org$
to exclude Org files.
And so on.
You can press i
(denote-search-only-include-files
) for the
opposite operation; it will prompt for a regular expression, and then
re-run the search, but only on the files matched by the regular
expression given.
These commands always act as if the files matched by the last search
were all the Denote files in existence. This has a great advantage;
you can chain them in any way you want. You can, for example, press
i
and input _emacs
, and then press x
and input _philosophy
.
The resulting buffer will have all the notes which have the keyword
“emacs” but not the keyword “philosophy”, and will then display the
matches for the search query you made originally only for those
specific files. You can of course keep filtering further.
To “break the chain” and start a totally new search, you can simply
call denote-search
, which is bound to s
in the results buffer for
convenience.
It’s possible that you don’t want to start a new search, but rather to search something on the curated file list you got. See focused search.
History is available when filtering. Press M-p
(previous-history-element
) to view past queries. This history is
kept separately from that available when searching.
A “focused search” is a search which is run against a set of files matched by a previous search. There are many use-cases for this, including searching for a note that you know has two or three very specific words, probably on different lines.
On the results buffer, press f
(denote-search-refine
) to start a
focused search. The command will prompt for a regular expression.
Once given, it will be searched in the files matched by the last
search. The buffer will be properly updated and will show the matches
found.
Note that this feature, combined with the filtering capabilities offered by this package (see filtering the search results), can be very powerful.
Let’s look at a complex example, which would be nearly impossible with other tools: search for all the mentions of “quantum mechanics” in notes with the “science” keyword, without the “personal” keyword, and which mention “Maxwell” somewhere in the text. The procedure is as follows:
M-x denote-search RET maxwell RET
[fn:3]i _science RET
x _personal RET
f quantum mechanics RET
There it is, a really complex task is done in four straightforward steps. And all without external and platform-specific programs such as xapian!
denote-search
is designed only for finding information in your
knowledge database, not for changing it. You can open the files (with
RET
, n
or otherwise) and edit them as usual.
There’s a special editing feature offered by Xref, though. You can
press r
(xref-query-replace-in-results
) to replace the search
query (naturally as a regular expression) in the files matched; the
interface is similar to that of query-replace-regexp
. This works in
the files then displayed in the results buffer, so you can filter and
fine-grain as usual to come with the replace command you wish. Maybe
replace all the occurences of “Vim” with “Emacs” in notes with the
keyword “programming” but not in notes with the keyword “personal”,
and only in files which mention “GNU”? The sky is the limit!
Although denote-search
is designed to be simple and require no
special configuration, some options are available.
By default, denote-search
uses the pretty title found in the
front-matter to format note headings in the results buffer. This is
pleasent to the eye and shouldn’t impact performance (the bottleneck
is always the search itself).
You can change how headings are formatted by customizing the
denote-search-format-heading-function
variable. It must be set to a
function which takes a single argument, the file path, and returns the
desired string for the heading.
By default, denote-search-extract-title
is used, which produces the
aforementioned behaviour. If it fails to find a suitable title for a
note, it uses the value of denote-search-untitled-string
. You can
customize that, as well.
This package also offers an alternative function, called
denote-search-format-heading-with-keywords
. If used as the heading
formatter, it adds keywords to the file title. This isn’t the default
merely to not clutter the view, but many users may prefer it.
You can write custom functions to display pretty much everything you want in the headings. Just keep in mind that the function is called for every single matched file, so for large collection of notes, it can indeed impact performance if you use complex code.
You can customize the name of the buffer where results are put using
the variable denote-search-buffer-name
.
By default, the results buffer has a header line which displays
information about the search and a short help string. Once you know
the commands by heart, you can set the variable
denote-search-help-string
to nil or a void string to disable the
help.
If silos (see denote#Maintain separate directory silos for notes) are
set up correctly (that is, with a .dir-locals.el
file that sets a
value for denote-directory
), then denote-search
should correctly
search in the contents of the silo when inside of it, without
additional configuration.
If you are using Emacs 29.1 onwards, you can install the package by evaluating the following code:
(package-vc-install
'(denote-search
:url "https://github.com/lmq-10/denote-search"
:doc "README.org"))
Alternatively, you can use the :vc keyword from use-package, as shown in the sample configuration.
Of course, you can also install it manually or use an alternative package manager such as quelpa.
Assuming your Emacs files are found in ~/.emacs.d/
, execute the
following commands in a shell prompt:
cd ~/.emacs.d
# Create a directory for manually-installed packages
mkdir manual-packages
# Go to the new directory
cd manual-packages
# Clone this repo, naming it "denote-search"
git clone https://github.com/lmq-10/denote-search denote-search
Finally, in your init.el
(or equivalent) evaluate this:
;; Make Elisp files in that directory available to the user.
(add-to-list 'load-path "~/.emacs.d/manual-packages/denote-search")
Everything is in place to set up the package.
(use-package denote-search
:ensure t
;; Installation with VC
:vc (:url "https://github.com/lmq-10/denote-search"
:rev :newest)
:bind
;; Customize keybindings to your liking
(("C-c s s" . denote-search)
("C-c s d" . denote-search-marked-dired-files)
("C-c s r" . denote-search-files-referenced-in-region))
:custom
;; Disable help string (set it once you learn the commands)
;; (denote-search-help-string "")
;; Display keywords in results buffer
(denote-search-format-heading-function #'denote-search-format-heading-with-keywords))
Fixes for some common issues.
Search is not managed by denote-search
, but rather by xref
. Check
the value of xref-search-program
. Changing it to ripgrep
(after
installing it of course) can improve the speed.
denote-search
, just like Denote itself, is meant to be a collective
effort. Every bit of help matters.
- Author/maintainer
- Lucas Quintana.
- Contributions to code
- Grant Rettke.
If denote-search
exists it’s because Protesilaos Stavrou developed
the incredible Denote package. Please consider donating to him.[fn:2]
I also want to thank Richard Stallman (creator of GNU Emacs), Po Lu
(who ported it to Android, allowing me to write the first version of
denote-search
) and Hiraoka Kazuyuki (author of Howm, from which this
package borrows some ideas). This wouldn’t be possible without them,
either.
[fn:3] You’ll probably bind denote-search
to something comfortable, see Sample configuration