Merge main features from denote-search into Denote

This commit is contained in:
Lucas Quintana 2025-03-29 14:13:20 -03:00
parent 9aed12156e
commit f70c96c5ca

274
denote.el
View file

@ -2539,17 +2539,21 @@ pass it to `denote-directory-files'."
(denote-retrieve-groups-xref-query query files-matching-regexp))
#'string-collate-lessp))
(defun denote-retrieve-xref-alist (query &optional files-matching-regexp)
(defun denote-retrieve-xref-alist (query &optional files)
"Return xref alist of absolute file paths with location of matches for QUERY.
With optional FILES-MATCHING-REGEXP, limit the list of files
accordingly (per `denote-directory-files').
Optional FILES can be a list of files to search for. It can also be a
regular expression, which means to use the text files in
`denote-directory' that match that regexp.
At all times limit the search to text files."
If FILES is not given, use all text files as returned by
`denote-directory-files'."
(let ((xref-file-name-display 'abs))
(xref--analyze
(xref-matches-in-files
query
(denote-directory-files files-matching-regexp :omit-current :text-only)))))
(if (and files (listp files))
files
(denote-directory-files files denote-query--omit-current :text-only))))))
;;;; New note
@ -5100,25 +5104,89 @@ file's title. This has the same meaning as in `denote-link'."
'denote-query-mode
"4.0.0")
(declare-function outline-cycle "outline" (&optional event))
(declare-function outline-cycle-buffer "outline" (&optional level))
(declare-function outline-next-heading "outline" ())
(declare-function outline-previous-heading "outline" ())
(defvar denote-query-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "a" #'outline-cycle-buffer)
(define-key map "f" #'denote-grep-refine)
(define-key map "k" #'outline-previous-heading)
(define-key map "j" #'outline-next-heading)
(define-key map "o" #'delete-other-windows)
(define-key map "s" #'denote-grep)
(define-key map "v" #'outline-cycle)
(define-key map "x" #'denote-grep-exclude-files)
(define-key map "i" #'denote-grep-only-include-files)
(define-key map "l" #'recenter-current-error)
(define-key map "X" #'denote-grep-exclude-files-with-keywords)
(define-key map "I" #'denote-grep-only-include-files-with-keywords)
(define-key map "G" #'denote-grep-clear-all-filters)
map)
"Keymap for buffers generated by `denote-make-links-buffer'.")
(define-derived-mode denote-query-mode xref--xref-buffer-mode "Denote Query"
"Major mode for queries found in the variable `denote-directory'.
This is used by the command `denote-backlinks' and all links created by
the `denote-query' command, among others."
:interactive nil)
:interactive nil
(require 'outline)
(setq-local outline-minor-mode-use-buttons 'in-margins)
(outline-minor-mode))
(define-obsolete-function-alias
'denote-link--prepare-backlinks
'denote-make-links-buffer
"4.0.0")
(defcustom denote-query-format-heading-function #'identity
"Function used to construct headings for files matched by a query.
It is called with a single argument, the path to the note file, and it
should always return a string."
:type 'function)
(defcustom denote-query-untitled-string "[Untitled]"
"String to use as heading for untitled notes in links' buffer.
Used only by `denote-query-extract-title'."
:type 'string)
(defvar denote-query--last-files nil
"Variable holding files matched by last call to `denote-make-links-buffer'.")
(defvar denote-query--last-query nil
"Variable holding query string for last call to `denote-make-links-buffer'.")
(defvar denote-query--omit-current t
"Variable governing whether query should omit current file.")
(defun denote-query-extract-title (file)
"Extract note title from FILE front matter.
When no title is found, return title found in FILE name.
When that doesn't work, return `denote-grep-untitled-string'.
Intended to be used as `denote-query-format-heading-function'."
(let ((title
(denote-retrieve-title-or-filename
file
(denote-filetype-heuristics file))))
(if (and (stringp title) (not (string-blank-p title)))
title
denote-query-untitled-string)))
;; NOTE 2025-03-24: The `&rest' is there because we used to have an
;; extra SHOW-CONTEXT parameter. This way we do not break anybody's
;; code, even if we slightly modify the behaviour.
(defun denote-make-links-buffer (query &optional files-matching-regexp buffer-name display-buffer-action &rest _)
(defun denote-make-links-buffer (query &optional files buffer-name display-buffer-action &rest _)
"Create links' buffer called BUFFER-NAME for QUERY.
With optional FILES-MATCHING-REGEXP, limit the list of files
accordingly (per `denote-directory-files').
Optional FILES can be a list of files to search for. It can also be a
regexp, which limits the files accordingly per `denote-directory-files'.
Optional DISPLAY-BUFFER-ACTION is a `display-buffer' action and
concomitant alist, such as `denote-backlinks-display-buffer-action'."
@ -5130,10 +5198,29 @@ concomitant alist, such as `denote-backlinks-display-buffer-action'."
;; suitable function and the results would be automatically
;; in relative form, but eventually notes may not be all
;; under a common directory (or project).
(xref-alist (denote-retrieve-xref-alist query files-matching-regexp))
(xref-alist (denote-retrieve-xref-alist query files))
(dir (denote-directory)))
(unless xref-alist
(error "No matches for query `%s'" query))
;; Update internal variables
(setq denote-query--last-files nil)
(setq denote-query--last-query query)
(dolist (x xref-alist)
(let* ((file-xref (car x))
(file
;; NOTE: Unfortunately, the car of the xref construct is
;; not reliable; sometimes it's absolute, sometimes it
;; is not
(if (file-name-absolute-p file-xref)
file-xref
(xref-location-group
(xref-match-item-location (car (last x)))))))
;; Add to current set of files
(push file denote-query--last-files)
;; Format heading
(setf (car x) (funcall denote-query-format-heading-function file))))
(delete-dups denote-query--last-files)
;; Insert results
(with-current-buffer (get-buffer-create buffer)
(erase-buffer)
(denote-query-mode)
@ -5150,13 +5237,178 @@ concomitant alist, such as `denote-backlinks-display-buffer-action'."
(setq-local revert-buffer-function
(lambda (_ignore-auto _noconfirm)
(when-let* ((buffer-file-name file))
(denote-make-links-buffer query files-matching-regexp buffer-name display-buffer-action)))))
(denote-make-links-buffer query files buffer-name display-buffer-action)))))
(display-buffer buffer display-buffer-action)))
(defvar denote-query-links-buffer-function #'denote-make-links-buffer
"Function to make an Xref buffer showing query link results.
It accepts the same arguments as `denote-make-links-buffer'.")
;;;;;; Additional features for searching file contents
(defvar denote-grep-query-history nil
"Minibuffer history for `denote-grep' commands.")
(defvar denote-grep-file-regexp-history nil
"Minibuffer history for `denote-grep' commands asking for a file regexp.")
(defun denote-grep-query-prompt (&optional type)
"Prompt for a search query in the minibuffer.
The prompt assumes a search in all files, unless TYPE is non-nil.
For now, the only recognized value for TYPE is :focused (for a focused
search, see `denote-grep-refine').
TYPE only affects the prompt, not the returned value.
Returned value is a list in order to be used in an `interactive' spec."
(list (read-string
(cond ((eq type :focused)
"Search (only files matched last): ")
(t "Search (all Denote files): "))
nil 'denote-grep-query-history)))
(defun denote-grep-file-regexp-prompt (&optional include)
"Prompt for a file regexp in the minibuffer.
The prompt assumes the user wants to exclude files, unless INCLUDE is
non-nil.
Returned value is a list in order to be used in an `interactive' spec."
(list (read-string
(if (not include)
"Exclude file names matching: "
"Only include file names matching: ")
nil 'denote-grep-file-regexp-history)))
(defun denote-grep-keywords-prompt (&optional include)
"Prompt for keywords to filter in the minibuffer, with completion.
Keywords are read using `completing-read-multiple'.
The prompt assumes the user wants to exclude the keywords, unless
INCLUDE is non-nil.
Returned value is a list in order to be used in an `interactive' spec."
(list
(delete-dups
(completing-read-multiple
(if (not include)
"Exclude files with keywords: "
"Only include files with keywords: ")
(denote-keywords) nil t nil 'denote-keyword-history))))
;;;###autoload
(defun denote-grep (query)
"Search QUERY in the content of Denote files.
QUERY should be a regular expression accepted by `xref-search-program'.
The files to search for are those returned by `denote-directory-files'
with a non-nil TEXT-ONLY argument.
Results are put in a buffer which allows folding and further
filtering (see the manual for details).
You can insert a link to a grep search in any note by using the command
`denote-query-contents-link'."
(interactive (denote-grep-query-prompt))
(let (denote-query--omit-current)
(denote-make-links-buffer query)))
(defun denote-grep-refine (query)
"Search QUERY in the content of files which matched the last search.
\"Last search\" here means any call to `denote-backlinks' and all links
created by the `denote-query' command.
QUERY should be regular expression."
(interactive (denote-grep-query-prompt :focused))
(denote-make-links-buffer
query denote-query--last-files
nil '(display-buffer-same-window))
(message "Searching `%s' in files matched previously" query))
(defun denote-grep-exclude-files (regexp)
"Exclude files whose name matches REGEXP from current search buffer.
This is useful even if you don't know regular expressions, given the
Denote file-naming scheme. For instance, to exclude notes with the
keyword \"philosophy\" from current search buffer, type
\\<denote-query-mode-map>\\[denote-grep-exclude-files] _philosophy
RET.
Internally, this works by generating a new call to
`denote-make-links-buffer' with the same QUERY as the last one, but with
a set of files gotten from checking REGEXP against last matched files.
When called from Lisp, REGEXP can be a list; in that case, it should be
a list of fixed strings (NOT regexps) to check against last matched
files. Files that match any of the strings get excluded. Internally,
the list is processed using `regexp-opt'. For an example of this usage,
see `denote-grep-exclude-files-with-keywords'."
(interactive (denote-grep-file-regexp-prompt))
(let (final-files)
(dolist (file denote-query--last-files)
(unless (string-match
;; Support list of strings as REGEXP
(if (listp regexp)
(regexp-opt regexp)
regexp)
file)
(push file final-files)))
(if final-files
(denote-make-links-buffer denote-query--last-query final-files)
(user-error "No remaining files when applying that filter"))
(message "Excluding files matching `%s'" regexp)))
(defun denote-grep-only-include-files (regexp)
"Exclude file names not matching REGEXP from current query buffer.
See `denote-grep-exclude-files' for details, including the behaviour
when REGEXP is a list."
(interactive (denote-grep-file-regexp-prompt :include))
(let (final-files)
(dolist (file denote-query--last-files)
(when (string-match
;; Support list of strings as REGEXP
(if (listp regexp)
(regexp-opt regexp)
regexp)
file)
(push file final-files)))
(if final-files
(denote-make-links-buffer denote-query--last-query final-files)
(user-error "No remaining files when applying that filter"))
(message "Only including files matching `%s'" regexp)))
(defun denote-grep-exclude-files-with-keywords (keywords)
"Exclude files with KEYWORDS from current query buffer.
KEYWORDS should be a list of keywords (without underscore).
Interactively, KEYWORDS are read from the minibuffer using
`completing-read-multiple', which see."
(interactive (denote-grep-keywords-prompt))
(denote-grep-exclude-files
(mapcar (lambda (kw) (concat "_" kw)) keywords)))
(defun denote-grep-only-include-files-with-keywords (keywords)
"Exclude files without KEYWORDS from current query buffer.
See `denote-grep-exclude-files-with-keywords' for details."
(interactive (denote-grep-keywords-prompt :include))
(denote-grep-only-include-files
(mapcar (lambda (kw) (concat "_" kw)) keywords)))
(defun denote-grep-clear-all-filters ()
"Run the last search with the full set of files in `denote-directory'.
This effectively gets ride of any interactive filter applied (by the
means of e.g. `denote-grep-exclude-files')."
(interactive)
(denote-make-links-buffer denote-query--last-query)
(message "Cleared all filters"))
;;;;;; Backlinks
(defun denote--backlinks-get-buffer-name (file id)