branch: externals/denote
commit a612e42567bffc7ef468fb63c24f83897e479a1e
Author: Protesilaos Stavrou <i...@protesilaos.com>
Commit: Protesilaos Stavrou <i...@protesilaos.com>

    Add initial support for query links
    
    I still need to work on the link implementation for file types other
    than Org (in 'denote-fontify-links' and related) and then expand the
    manual to document all the changes herein. Though I want to make this
    commit first to give others the chance to review the code.
---
 denote.el | 341 +++++++++++++++++++++++++++++---------------------------------
 1 file changed, 157 insertions(+), 184 deletions(-)

diff --git a/denote.el b/denote.el
index 6cf052d23a..fbfb323e52 100644
--- a/denote.el
+++ b/denote.el
@@ -593,18 +593,7 @@ command."
   :link '(info-link "(denote) The denote-templates option")
   :group 'denote)
 
-(defcustom denote-backlinks-show-context nil
-  "When non-nil, show link context in the backlinks buffer.
-
-The context is the line a link to the current note is found in.
-The context includes multiple links to the same note, if those
-are present.
-
-When nil, only show a simple list of file names that link to the
-current note."
-  :group 'denote
-  :package-version '(denote . "1.2.0")
-  :type 'boolean)
+(make-obsolete-variable 'denote-backlinks-show-context nil "4.0.0")
 
 (make-obsolete-variable 'denote-rename-no-confirm 'denote-rename-confirmations 
"3.0.0")
 
@@ -648,6 +637,24 @@ and/or the documentation string of `display-buffer'."
   :package-version '(denote . "3.1.0")
   :group 'denote)
 
+(defcustom denote-query-links-display-buffer-action
+  '((display-buffer-reuse-window display-buffer-below-selected)
+    (window-height . fit-window-to-buffer)
+    (dedicated . t))
+  "The action used to display query links.
+This is the same as `denote-backlinks-display-buffer-action'.  Refer to
+its documentation for the technicalities."
+  :risky t
+  :type `(choice
+          (alist :key-type
+                 (choice :tag "Condition"
+                         regexp
+                         (function :tag "Matcher function"))
+                 :value-type ,display-buffer--action-custom-type)
+          (function :tag "Custom function to return an action alist"))
+  :package-version '(denote . "4.0.0")
+  :group 'denote)
+
 (defcustom denote-rename-confirmations '(rewrite-front-matter modify-file-name)
   "Make renaming commands prompt for confirmations.
 
@@ -2508,10 +2515,11 @@ This is a wrapper for 
`denote-retrieve-front-matter-title-value' and
   'denote-retrieve-groups-xref-query
   "4.0.0")
 
-(defun denote-retrieve-groups-xref-query (query)
+(defun denote-retrieve-groups-xref-query (query &optional 
files-matching-regexp)
   "Access location of xrefs for QUERY and group them per file.
-Limit the search to text files."
-  (when-let* ((files (denote-directory-files nil nil :text-only))
+Limit the search to text files.  With optional FILES-MATCHING-REGEXP,
+pass it to `denote-directory-files'."
+  (when-let* ((files (denote-directory-files files-matching-regexp nil 
:text-only))
               (locations (mapcar #'xref-match-item-location 
(xref-matches-in-files query files))))
     (mapcar #'xref-location-group locations)))
 
@@ -2520,16 +2528,17 @@ Limit the search to text files."
   'denote-retrieve-files-xref-query
   "4.0.0")
 
-(defun denote-retrieve-files-xref-query (query)
+(defun denote-retrieve-files-xref-query (query &optional files-matching-regexp)
   "Return sorted, deduplicated file names with matches for QUERY in their 
contents.
-Limit the search to text files."
+Limit the search to text files.  With optional FILES-MATCHING-REGEXP,
+pass it to `denote-directory-files'."
   (sort
    (delete-dups
-    (denote-retrieve-groups-xref-query query))
+    (denote-retrieve-groups-xref-query query files-matching-regexp))
    #'string-collate-lessp))
 
 (defun denote-retrieve-xref-alist (query &optional files-matching-regexp)
-  "Return xref alist of files with location of matches for QUERY.
+  "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').
 
@@ -4613,9 +4622,7 @@ and seconds."
   `((denote-faces-dired-file-name-matcher ,@denote-faces-matchers))
   "Keywords for fontification of file names.")
 
-(defconst denote-faces-file-name-keywords-for-backlinks
-  `(("^.+$" ,@denote-faces-matchers))
-  "Keywords for fontification of file names.")
+(make-obsolete-variable 'denote-faces-file-name-keywords-for-backlinks nil 
"4.0.0")
 
 (defface denote-faces-prompt-old-name '((t :inherit error))
   "Face for the old name shown in the prompt of `denote-rename-file' etc."
@@ -5245,162 +5252,72 @@ major mode is not `org-mode' (or derived therefrom).  
Consider using
                  thing-at-point-provider-alist)))
   (font-lock-update))
 
-;;;;; Backlinks' buffer
+;;;;; Links' buffer (query links and backlinks using `denote-query-mode')
 
-(define-button-type 'denote-link-backlink-button
-  'follow-link t
-  'action #'denote-link--backlink-find-file
-  'face nil) ; we add fontification in `denote-link--prepare-backlinks'
-
-(defun denote-link--backlink-find-file (button)
-  "Action for BUTTON to `find-file'."
-  (funcall denote-open-link-function
-           (concat (denote-directory)
-                   (buffer-substring (button-start button) (button-end 
button)))))
-
-(defun denote-link--display-buffer (buf &optional action)
-  "Run `display-buffer' on BUF using optional ACTION alist.
-ACTION is an alist of the form described in the user option
-`denote-backlinks-display-buffer-action'."
-  (display-buffer
-   buf
-   `(,@(or action denote-backlinks-display-buffer-action))))
+(make-obsolete 'denote-link--backlink-find-file nil "4.0.0")
+(make-obsolete 'denote-link--display-buffer nil "4.0.0")
+(make-obsolete 'denote-backlinks-mode-next nil "4.0.0")
+(make-obsolete 'denote-backlinks-mode-previous nil "4.0.0")
+(make-obsolete 'denote-backlinks-toggle-context nil "4.0.0")
+(make-obsolete-variable 'denote-backlinks-mode-map nil "4.0.0")
 
 (define-obsolete-function-alias
-  'denote-backlinks-next
-  'denote-backlinks-mode-next
-  "2.3.0")
-
-(defun denote-backlinks-mode-next (n)
-  "Use appropriate command for forward motion in backlinks buffer.
-With N as a numeric argument, move to the Nth button from point.
-A nil value of N is understood as 1.
-
-When `denote-backlinks-show-context' is nil, move between files
-in the backlinks buffer.
+  'denote-backlinks-mode
+  'denote-query-mode
+  "4.0.0")
 
-When `denote-backlinks-show-context' is non-nil move between
-matching identifiers."
-  (interactive "p" denote-backlinks-mode)
-  (unless (derived-mode-p 'denote-backlinks-mode)
-    (user-error "Only use this in a Denote backlinks buffer"))
-  (if denote-backlinks-show-context
-      (xref-next-line)
-    (forward-button n)))
+(define-derived-mode denote-query-mode xref--xref-buffer-mode "Denote"
+  "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)
 
 (define-obsolete-function-alias
-  'denote-backlinks-prev
-  'denote-backlinks-mode-previous
-  "2.3.0")
-
-(defun denote-backlinks-mode-previous (n)
-  "Use appropriate command for backward motion in backlinks buffer.
-With N as a numeric argument, move to the Nth button from point.
-A nil value of N is understood as 1.
-
-When `denote-backlinks-show-context' is nil, move between files
-in the backlinks buffer.
-
-When `denote-backlinks-show-context' is non-nil move between
-matching identifiers."
-  (interactive "p" denote-backlinks-mode)
-  (unless (derived-mode-p 'denote-backlinks-mode)
-    (user-error "Only use this in a Denote backlinks buffer"))
-  (if denote-backlinks-show-context
-      (xref-prev-line)
-    (backward-button n)))
-
-;; NOTE 2024-07-25: This can be a minor mode, though I do not like
-;; that global minor modes have to be autoloaded.  We do not need to
-;; autoload a secondary piece of functionality.
-;;
-;; NOTE 2024-07-25: We do not need the user option if we turn this
-;; into a minor mode.
-;;
-;; NOTE 2024-07-25: I would prefer to have a buffer-local toggle which
-;; does not affect the global user preference.  The trick is to make
-;; this work with `revert-buffer'.
-(defun denote-backlinks-toggle-context ()
-  "Show or hide the context of links in backlinks buffers.
-This is the same as toggling the `denote-backlinks-show-context' user
-option.
-
-When called inside of a backlinks buffer, also revert the buffer."
-  (interactive)
-  (let ((state))
-    (if denote-backlinks-show-context
-        (setq denote-backlinks-show-context nil
-              state "compact")
-      (setq denote-backlinks-show-context t
-            state "detailed"))
-    (message "Toggled the %s view for the backlinks buffer"
-             (propertize state 'face 'error))
-    (when (derived-mode-p 'denote-backlinks-mode)
-      (revert-buffer)
-      (fit-window-to-buffer))))
-
-(defvar denote-backlinks-mode-map
-  (let ((m (make-sparse-keymap)))
-    (define-key m "n" #'denote-backlinks-mode-next)
-    (define-key m "p" #'denote-backlinks-mode-previous)
-    (define-key m "c" #'denote-backlinks-toggle-context)
-    (define-key m "g" #'revert-buffer)
-    m)
-  "Keymap for `denote-backlinks-mode'.")
-
-(define-derived-mode denote-backlinks-mode xref--xref-buffer-mode "Backlinks"
-  "Major mode for backlinks buffers."
-  :interactive nil)
+  'denote-link--prepare-backlinks
+  'denote-make-links-buffer
+  "4.0.0")
 
-(defun denote-link--prepare-backlinks (query &optional files-matching-regexp 
buffer-name display-buffer-action show-context)
-  "Create backlinks' buffer called BUFFER-NAME for the current file matching 
QUERY.
+;; 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 _)
+  "Create links' buffer called BUFFER-NAME for QUERY.
 
 With optional FILES-MATCHING-REGEXP, limit the list of 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'.
-
-Optional SHOW-CONTEXT displays the lines where matches for QUERY
-occur.  This is the same as setting `denote-backlinks-show-context' to a
-non-nil value."
+concomitant alist, such as `denote-backlinks-display-buffer-action'."
   (let* ((inhibit-read-only t)
-         (file (buffer-file-name))
-         (backlinks-buffer (or buffer-name (format "Backlinks for '%s'" 
query)))
+         (file buffer-file-name)
+         (buffer (or buffer-name (format-message "Denote query for `%s'" 
query)))
          ;; We retrieve results in absolute form and change the
          ;; absolute path to a relative path below. We could add a
          ;; 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-file-name-display 'abs)
          (xref-alist (denote-retrieve-xref-alist query files-matching-regexp))
          (dir (denote-directory)))
     (unless xref-alist
-      (error "No backlinks for query `%s'" query))
-    (with-current-buffer (get-buffer-create backlinks-buffer)
+      (error "No matches for query `%s'" query))
+    (with-current-buffer (get-buffer-create buffer)
       (erase-buffer)
-      (denote-backlinks-mode)
-      ;; In the backlinks buffer, the values of variables set in a
-      ;; `.dir-locals.el` do not apply.  We need to set `denote-directory' in
-      ;; the backlinks buffer because the buttons depend on it.  Moreover, its
-      ;; value is overwritten after enabling the major mode, so it needs to be
-      ;; set after.
+      (denote-query-mode)
+      ;; In the links' buffer, the values of variables set in a
+      ;; `.dir-locals.el` do not apply.  We need to set
+      ;; `denote-directory' here because the buttons depend on it.
+      ;; Moreover, its value is overwritten after enabling the major
+      ;; mode, so it needs to be set after.
       (setq-local denote-directory dir)
       (setq overlay-arrow-position nil)
       (goto-char (point-min))
-      (if (or show-context denote-backlinks-show-context)
-          (xref--insert-xrefs xref-alist)
-        (dolist (element xref-alist)
-          (insert (denote-get-file-name-relative-to-denote-directory (car 
element)))
-          (make-button (line-beginning-position) (line-end-position) :type 
'denote-link-backlink-button)
-          (insert "\n"))
-        (font-lock-add-keywords nil 
denote-faces-file-name-keywords-for-backlinks t))
+      (xref--insert-xrefs xref-alist)
       (goto-char (point-min))
       (setq-local revert-buffer-function
                   (lambda (_ignore-auto _noconfirm)
                     (when-let* ((buffer-file-name file))
-                      (denote-link--prepare-backlinks query 
files-matching-regexp buffer-name display-buffer-action show-context)))))
-    (denote-link--display-buffer backlinks-buffer display-buffer-action)))
+                      (denote-make-links-buffer query files-matching-regexp 
buffer-name display-buffer-action)))))
+    (display-buffer buffer display-buffer-action)))
 
 (defun denote--backlinks-get-buffer-name (file id)
   "Format a buffer name for `denote-backlinks'.
@@ -5423,13 +5340,63 @@ Place the buffer below the current window or wherever 
the user option
 `denote-backlinks-display-buffer-action' specifies."
   (interactive)
   (if-let* ((file buffer-file-name))
-      (when-let* ((id (denote-retrieve-filename-identifier-with-error file)))
-        (denote-link--prepare-backlinks id nil 
(denote--backlinks-get-buffer-name file id)))
+      (when-let* ((identifier (denote-retrieve-filename-identifier-with-error 
file)))
+        (denote-make-links-buffer
+         identifier nil
+         (denote--backlinks-get-buffer-name file identifier)
+         denote-backlinks-display-buffer-action))
     (user-error "Buffer `%s' is not associated with a file" (current-buffer))))
 
 (defalias 'denote-show-backlinks-buffer 'denote-backlinks
   "Alias for `denote-backlinks' command.")
 
+(defvar denote-query-history nil
+  "Minibuffer history of `denote-query-prompt'.")
+
+(defun denote-query-prompt (&optional initial-query prompt-text)
+  "Prompt for query string.
+With optional INITIAL-QUERY use it as the initial minibuffer text.  With
+optional PROMPT-TEXT use it in the minibuffer instead of the default
+prompt.
+
+Previous inputs at this prompt are available for minibuffer completion
+if the user option `denote-history-completion-in-prompts' is set to a
+non-nil value."
+  (when (and initial-query (string-empty-p initial-query))
+    (setq initial-query nil))
+  (denote--with-conditional-completion
+   'denote-signature-prompt
+   (format-prompt (or prompt-text "Query in files") nil)
+   denote-query-history
+   initial-query))
+
+;;;###autoload
+(defun denote-query (query &optional files-matching-regexp)
+  "Create a QUERY link at point.
+Query links do not point to any file but instead initiate a search in
+the contents of files inside the variable `denote-directory'.  They are
+always formatted as [[denote:QUERY]].  This is unlike what `denote-link'
+and related commands do, which always establish a direct connection to a
+file and their format is more flexible.
+
+With optional FILES-MATCHING-REGEXP, limit the list of files to search
+through to only those whose file name matches the given regular
+expression.  When called interactively, prompt FILES-MATCHING-REGEXP
+when there is a universal prefix argument (\\[universal-argument])."
+  (interactive
+   (list
+    (denote-query-prompt)
+    (when current-prefix-arg
+      ;; NOTE 2025-03-24: I think we do not need a prompt for this
+      ;; one.  But if we do, then it probably should be like
+      ;; `denote-query-prompt'.
+      (read-string "Limit to FILES-MATCHING-REGEXP: "))))
+  (if-let* ((files (denote-retrieve-files-xref-query query 
files-matching-regexp)))
+      (progn
+        (denote--delete-active-region-content)
+        (insert (format "[[denote:%s]]" query)))
+    (user-error "No files with matching `%s'" query)))
+
 ;;;;; Add links matching regexp
 
 (defvar denote-link--prepare-links-format "- %s\n"
@@ -5666,33 +5633,37 @@ This command is meant to be used from a Dired buffer."
 (declare-function org-link-open-as-file "ol" (path arg))
 
 (defun denote-link--ol-resolve-link-to-target (link &optional full-data)
-  "Resolve LINK to target file, with or without additioanl query terms.
-With optional FULL-DATA return a list in the form of (path id query)."
-  (let* ((query (and (string-match "::\\(.*\\)\\'" link)
-                     (match-string 1 link)))
-         (id (if (and query (not (string-empty-p query)))
-                 (substring link 0 (match-beginning 0))
-               link))
-         (path (denote-get-path-by-id id)))
+  "Resolve LINK to target file, with or without additioanl file-search terms.
+With optional FULL-DATA return a list in the form of (path query file-search)."
+  (let* ((file-search (and (string-match "::\\(.*\\)\\'" link)
+                           (match-string 1 link)))
+         (query (if (and file-search (not (string-empty-p file-search)))
+                    (substring link 0 (match-beginning 0))
+                  link))
+         (path (denote-get-path-by-id query)))
     (cond
      (full-data
-      (list path id query))
-     ((and query (not (string-empty-p query)))
-      (concat path "::" query))
-     (t path))))
+      (list path query file-search))
+     ((and file-search (not (string-empty-p file-search)))
+      (concat path "::" file-search))
+     (t (or path query)))))
 
 ;;;###autoload
 (defun denote-link-ol-follow (link)
   "Find file of type `denote:' matching LINK.
-LINK is the identifier of the note, optionally followed by a
-query option akin to that of standard Org `file:' link types.
-Read Info node `(org) Query Options'.
+LINK is the identifier of the note, optionally followed by a file search
+option akin to that of standard Org `file:' link types.  Read Info
+node `(org) Query Options'.
+
+If LINK is not an identifier, then it is not pointing to a file but to a
+query of file contents.  Those are displayed in a separate buffer which
+uses `denote-query-mode'.
 
-Uses the function `denote-directory' to establish the path to the
-file."
-  (org-link-open-as-file
-   (denote-link--ol-resolve-link-to-target link)
-   nil))
+Uses the function `denote-directory' to establish the path to the file."
+  (if-let* ((match (denote-link--ol-resolve-link-to-target link))
+            (_ (file-exists-p match)))
+      (org-link-open-as-file match nil)
+    (denote-make-links-buffer match nil nil 
denote-query-links-display-buffer-action)))
 
 ;;;###autoload
 (defun denote-link-ol-complete ()
@@ -5770,21 +5741,23 @@ Also see the user option 
`denote-org-store-link-to-heading'."
   "Export a `denote:' link from Org files.
 The LINK, DESCRIPTION, and FORMAT are handled by the export
 backend."
-  (pcase-let* ((`(,path ,id ,query) (denote-link--ol-resolve-link-to-target 
link :full-data))
-               (anchor (file-relative-name (file-name-sans-extension path)))
+  (pcase-let* ((`(,path ,query ,file-search) 
(denote-link--ol-resolve-link-to-target link :full-data))
+               (anchor (when path (file-relative-name 
(file-name-sans-extension path))))
                (desc (cond
                       (description)
-                      (query (format "denote:%s::%s" id query))
-                      (t (concat "denote:" id)))))
-    (pcase format
-      ('html (if query
-                 (format "<a href=\"%s.html%s\">%s</a>" anchor query desc)
-               (format "<a href=\"%s.html\">%s</a>" anchor desc)))
-      ('latex (format "\\href{%s}{%s}" (replace-regexp-in-string 
"[\\{}$%&_#~^]" "\\\\\\&" path) desc))
-      ('texinfo (format "@uref{%s,%s}" path desc))
-      ('ascii (format "[%s] <denote:%s>" desc path))
-      ('md (format "[%s](%s)" desc path))
-      (_ path))))
+                      (file-search (format "denote:%s::%s" query file-search))
+                      (t (concat "denote:" query)))))
+    (if path
+        (pcase format
+          ('html (if file-search
+                     (format "<a href=\"%s.html%s\">%s</a>" anchor file-search 
desc)
+                   (format "<a href=\"%s.html\">%s</a>" anchor desc)))
+          ('latex (format "\\href{%s}{%s}" (replace-regexp-in-string 
"[\\{}$%&_#~^]" "\\\\\\&" path) desc))
+          ('texinfo (format "@uref{%s,%s}" path desc))
+          ('ascii (format "[%s] <denote:%s>" desc path))
+          ('md (format "[%s](%s)" desc path))
+          (_ path))
+      (format-message "[[Denote query for `%s']]" query))))
 
 (defun denote-link-ol-help-echo (_window _object position)
   "Echo the full file path of the identifier at POSITION."

Reply via email to